# Store6 guide corpus Includes Store6 guides and agent setup. Store5 and generated Dokka API HTML are excluded; linked references remain available. # Agent skills Canonical page: https\://store.mobilenativefoundation.org/docs/store6/agents/agent-skills Markdown: https\://store.mobilenativefoundation.org/llms/store6/agents/agent-skills.md Source kind: site-authored; source path: content/docs/store6/agents/agent-skills.mdx Install the Store6 coding skill and retrieve documentation for your project's source version. The Store6 skill provides task routing and a documentation retrieval helper. It checks the project's Store6 identity, selects relevant guides, and requires citations and appropriate verification when generating code. It retrieves current pages only when they match its packaged documentation manifest. ## Prerequisites Use a coding agent that supports the [Agent Skills format](https://agentskills.io/specification), Node.js 22.18 or newer, and network access to the documentation site. The retrieval helper uses Node's built-in APIs and needs no additional package installation. ## Install in your project The skill lives in [matt-ramotar/store-agent-skills](https://github.com/matt-ramotar/store-agent-skills) as a development candidate. Its paired Markdown bundle is not yet available on the public documentation site, so retrieval cannot succeed yet. Use **Copy Markdown** for page context meanwhile. **Copy Prompt** checks that the repository exposes the `store6` skill before installing. To install the candidate, run this command from your project directory using the [skills CLI](https://github.com/vercel-labs/skills): ```sh npx skills add matt-ramotar/store-agent-skills ``` Select `store6` and your coding agent when prompted. Use project scope and choose Copy for the installation method. To select an agent explicitly, append `--agent codex` or `--agent claude-code`. Review the installer's reported destination, then start a fresh agent session in that project. This command installs from the repository's default branch. The installed skill retains its packaged documentation manifest and still requires matching source and content. For a reproducible installation, use a reviewed commit or release URL and record the installed source revision. ### Cursor Install the candidate from your project directory: ```sh npx skills add matt-ramotar/store-agent-skills --agent cursor ``` Start a fresh agent session, then find `store6` in Cursor's skills or slash menu. Follow the version and retrieval checks below before asking it to generate code. ### VS Code Use GitHub Copilot's agent skills support. Install the candidate from your project directory: ```sh npx skills add matt-ramotar/store-agent-skills --agent github-copilot ``` Start a fresh Copilot chat and select `/store6`. See [VS Code's agent skills guide](https://code.visualstudio.com/docs/agent-customization/agent-skills) if the skill is not discovered. Follow the same version and retrieval checks below. ## Invoke the skill * **Codex:** select `store6` from `/skills` or include `$store6` in your request. * **Claude Code:** invoke `/store6`. * **Cursor:** find `store6` under Customize → Skills and select it from the slash menu. Ask the agent to identify the installed skill path and list its available guide IDs. It can run `node scripts/get-docs.mjs --list` from the installed skill directory without network access. ## Match your project before coding Ask the agent to inspect your dependency declaration or source checkout. The helper accepts one `--source-revision` or `--coordinate` argument followed by one to four guide IDs. A coordinate must be explicitly listed in the packaged manifest. A full source revision must match the manifest exactly; copying the documentation revision into a prompt does not establish your project's identity. Mutable `SNAPSHOT` labels and an unlisted Store6 version are insufficient. When a match cannot be established, resolve the version or supply matching documentation before generating code. Useful tasks include building a typed repository, adding pull-to-refresh, selecting a persistence adapter, binding collection to a UI lifetime, preserving queued mutations across restart, and migrating a Store5 repository. State the desired behavior and platform, and ask for the contracts used and the checks actually run. ## Handle retrieval failures | Error | Next step | | ------------------ | ---------------------------------------------------------------------------------------------------------- | | `VERSION_MISMATCH` | Verify the project's dependency or source revision and obtain matching documentation. | | `BUNDLE_MISMATCH` | Choose an explicitly updated skill release paired with the documentation, or provide matching context. | | `CONTENT_MISMATCH` | Stop using the returned context and verify the document source; its bytes differ from the packaged record. | | `NETWORK_ERROR` | Restore access and request the guides again. The helper returns no partial page set. | To update, deliberately repeat the installation and version checks. For installations from a fixed commit or release, explicitly select a newer tested ref. A site build does not update the skill's packaged manifest. ## Source and license The skill is licensed under Apache-2.0. Its source, license, and notices are maintained in [matt-ramotar/store-agent-skills](https://github.com/matt-ramotar/store-agent-skills). The skill links to Store6 documentation rather than maintaining a second API manual. --- # LLM context Canonical page: https\://store.mobilenativefoundation.org/docs/store6/agents/llm-context Markdown: https\://store.mobilenativefoundation.org/llms/store6/agents/llm-context.md Source kind: site-authored; source path: content/docs/store6/agents/llm-context.mdx Find Markdown guides and understand their recorded source identity. ## Find the right amount of documentation [/llms.txt](https://store.mobilenativefoundation.org/llms.txt) is the discovery index. It links to individual guides and the complete Store6 guide corpus. It does not contain the complete documentation itself. Use **View as Markdown** from a page's dropdown or open a URL such as [/llms/store6/quickstart.md](https://store.mobilenativefoundation.org/llms/store6/quickstart.md). **Copy Markdown** copies that complete Markdown document, including its title and source information, when your browser permits clipboard access. **Copy Prompt** copies Store6 environment setup instructions: check skill availability, install the skill in the project, and verify discovery and documentation retrieval. Paste it into your coding agent. The dropdown also links to editor setup guides and opens ChatGPT or Claude with the page's public Markdown URL in a prompt. These chats use the public site, so they cannot read unpublished local preview changes. If the page is unavailable to the chat, paste its content with **Copy Markdown**. For a tool with a larger context window, [/llms-full.txt](https://store.mobilenativefoundation.org/llms-full.txt) contains all Store6 guides and agent setup pages. It excludes Store5 documentation and generated Dokka API HTML. Links to those references remain available. ## Read the provenance [/llms/store6-manifest.json](https://store.mobilenativefoundation.org/llms/store6-manifest.json) records the bundle identity, Store6 source revision, exact page hashes, and canonical and Markdown URLs. * A `source-synced` page records its upstream path, revision, and source-file hash. * A `site-authored` page records its documentation source path and hash. Its `sourceRevision` is `null` unless that page has individually evidenced source attribution. * A visible **Source recorded** line preserves the page's authored attribution. It does not assert a new verification date or a broader compatibility guarantee. The page hash describes the emitted Markdown bytes. The bundle identity also includes the inputs that supply component text and diagrams. The [skill](https://store.mobilenativefoundation.org/llms/store6/agents/agent-skills.md) checks these identities before returning retrieved pages. ## Follow links and select context Links between complete Store6 guides lead to Markdown when possible. Links to specific sections keep their canonical HTML destinations so the section anchors remain usable. API links continue to the generated reference documentation. Select the guides for the task: the read and lifetime contracts for a first repository, freshness for refresh behavior, adapter documentation for persistence, or the journal and restart guides for durable mutations. Ask for citations to the applicable contracts and keep the project's actual source revision or dependency coordinate in the prompt. ## Update references deliberately The live guides can change as their sources change. Reopen the index when selecting new context. If an installed skill reports `BUNDLE_MISMATCH`, select an explicitly updated, matching skill release or supply matching documentation. The skill does not silently replace its recorded bundle. --- # Agents and LLMs Canonical page: https\://store.mobilenativefoundation.org/docs/store6/agents/overview Markdown: https\://store.mobilenativefoundation.org/llms/store6/agents/overview\.md Source kind: site-authored; source path: content/docs/store6/agents/overview\.mdx Give a coding agent Store6 documentation that matches your project. Use Store6 documentation as context when asking an agent to implement reads, persistence, mutations, UI collection, or a migration. Start by identifying the Store6 dependency or source revision in your project. A matching name or major version alone does not establish API compatibility. ## Choose your context | Method | Use it when | | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | **Copy Prompt** | You want a coding agent to check skill availability, install the Store6 skill in your project, and verify its setup. | | **Copy Markdown** | You want to paste one complete guide into a conversation. | | **View as Markdown** (in the dropdown) | Your agent can open a documentation URL and select the pages it needs. | | **Set up in Cursor / VS Code** (in the dropdown) | You want the skill setup guide for your editor. | | **Open in ChatGPT / Claude** (in the dropdown) | You want to ask questions about the page in a new chat. The link includes its public Markdown URL. | | [Store6 skill](https://store.mobilenativefoundation.org/llms/store6/agents/agent-skills.md) | Your coding agent supports skills and should check the project version before retrieving task-specific guides. | The page actions appear on Store6 guides with a Markdown export. See [LLM context](https://store.mobilenativefoundation.org/llms/store6/agents/llm-context.md) for the index, complete guide corpus, and source records. ## Start with a concrete task Give the agent the repository, desired behavior, and relevant platform. For example: **Example prompt** ```text Inspect this project's Store6 dependency or source revision. Use the matching Store6 documentation from https://store.mobilenativefoundation.org/llms.txt to implement a typed user repository. Cite the read and lifetime contracts, then run the project's relevant checks. If the version is unsupported, identify the mismatch before generating code. ``` For a persistence or mutation task, state what must survive a restart and which database and platforms your application uses. Ask the agent to distinguish what it checked in source from what it compiled or exercised in a test. ## Check the source identity The documentation manifest records the Store6 source revision and each page's provenance. Some pages are synchronized from that source; others are authored in the documentation site and retain their own source record. The shared revision does not mean every page was verified against it. The skill accepts a matching full source revision or an exact dependency coordinate that its manifest explicitly lists as verified. An unlisted version, including a mutable `SNAPSHOT`, is a mismatch. Resolve the dependency or provide matching documentation before asking it to generate code. --- # store6-compose Canonical page: https\://store.mobilenativefoundation.org/docs/store6/compose Markdown: https\://store.mobilenativefoundation.org/llms/store6/compose.md Source kind: source-synced; source path: store6-compose/README.md Source revision: ad435df1095673709a22f1b52a82aa03748cd9b3 Compose Multiplatform integration for Store v6. Everything here is `@ExperimentalStoreApi`. The seam it consumes is a freeze candidate, not frozen. See [STABILITY.md](https://store.mobilenativefoundation.org/llms/store6/stability.md). ## Entry points * `Store.collectAsState(key, freshness)`: `State>`, starts at `Loading`, restarts only on structural identity change (namespace/canonicalId/freshness), all targets. * `Flow>.collectAsStoreState(initial)`: the flow-level variant. * `Store.collectAsStateWithLifecycle(...)` / `collectAsStoreStateWithLifecycle(...)`: lifecycle-gated via `repeatOnLifecycle`, on all targets. These need a `LifecycleOwner`; on targets with no UI host that populates `LocalLifecycleOwner`, pass one explicitly. * `skipEqualData()` / `storeResultMutationPolicy()`: structural skipping for stateIn/ViewModel flows and custom state holders. ## Recomposition discipline `StoreResult` types deliberately have identity equality. This module skips recomposition by structural comparison of `Data`'s value/origin/isStale/refreshing. `age` is excluded (it advances every emission). Results are never merged across kinds. That mirrors the engine's `conflateLatestData` rule (same-kind latest-wins; never merged across kinds): "Revalidated is a lifecycle signal: `conflateLatestData` never conflates it away in favor of another kind; for a blocked collector a newer `Revalidated` supersedes an older queued one, so the kind itself is never lost." This module is stricter still: `Loading`/`Revalidated`/ `Error` always pass; only structurally-equal consecutive `Data` frames are dropped. Event-shaped consumption of `Revalidated`/`Error` should collect the Flow, not a State. ## Stability configuration for consumers Strong skipping (default since Kotlin 2.0.20) compares unstable parameters by instance; this module's state holders keep instances stable across equal frames, so skipping works out of the box. To make store types compare as stable values instead (which is what lets the compiler skip on equal *content* rather than equal *instance*), add the shipped snippet (`stability/store6-stability.conf`, reproduced below) to your app module: ```kotlin composeCompiler { stabilityConfigurationFiles.add( layout.projectDirectory.file("store6-stability.conf"), ) } ``` ```text // store6-stability.conf (mirror of the shipped file) org.mobilenativefoundation.store6.core.* org.mobilenativefoundation.store6.core.seam.* ``` CI verifies this exact snippet against a tiered probe of core public types on every PR. With the snippet applied, every probed core type, including the interface-typed ones (`StoreResult`, `Freshness`, `StoreKey`, `StoreMeta`, `StoreError`) and the generic `StoreResult.Data`, resolves as **stable**; without it, they resolve as `unstable` and the CI gate fails. Note that `composeCompiler.stabilityConfigurationFiles` is not registered as a Gradle task input by the Compose compiler plugin, and the emitted stability reports are an undeclared output. This module's build scripts compensate (`inputs.file(...)` plus opting the demo compilations out of the build cache) so that editing the conf always re-emits a matching report; consumers relying on their own report-based checks should do the same. ## Demo `./gradlew :store6-compose-demo:run`: refreshing spinner-over-content, STALE badge, and error-with-stale-data against a fake fetcher with toggleable latency and failure. --- # API tiers and opt-in annotations Canonical page: https\://store.mobilenativefoundation.org/docs/store6/concepts/api-tiers Markdown: https\://store.mobilenativefoundation.org/llms/store6/concepts/api-tiers.md Source kind: site-authored; source path: content/docs/store6/concepts/api-tiers.mdx What @ExperimentalStoreApi, @DelicateStoreApi, and @InternalStoreApi mean, and where the stability tiers actually live. When the compiler refuses an API and names an annotation, it is enforcing the stability tiers. Store 6 marks every API that carries less than the full stable guarantee with one of three opt-in markers, and all three are declared at `RequiresOptIn.Level.ERROR`. Using a marked API without opting in is a compile error, not a warning. The markers are real annotations in `store6-core`, in the `org.mobilenativefoundation.store6.core` package, and each one's meaning is carried in its own KDoc. The sections below restate them and show where you will meet each one. The full surface is browsable in the [store6-core API reference](https://store.mobilenativefoundation.org/reference/store6-core/index.html). ## `@ExperimentalStoreApi`: may change in any release Marks API that is under active development and **may change or be removed in any release**. Experimental API ships in separate artifacts wherever possible. This marker exists for the cases where an experimental member must live beside stable API. The compiler says: > This Store API is experimental and may change or be removed in any release. Inside `store6-core` you will hit it in exactly two places: * **Every type in the seam package**: the interfaces you implement to plug in your own infrastructure. There is one deliberate exception, covered below. * **Every expert knob on the builder**: the regular-interface `fetcher(Fetcher)` overload, `persistence`, `telemetry`, `overlay`, `bookkeeper`, `wallClock`, and `freshnessValidator`. The zero-config path needs no opt-in. `store { }` itself, the lambda `fetcher { }`, `fetcherOfResult`, and `maxIdleKeys` are all stable-track: you can build and run a complete store without a single annotation. The opt-in appears at the moment you reach past the defaults: ```kotlin import org.mobilenativefoundation.store6.core.ExperimentalStoreApi import org.mobilenativefoundation.store6.core.store @OptIn(ExperimentalStoreApi::class) val users = store { fetcher { key -> api.getUser(key.id) } // stable — needs no opt-in persistence(userSourceOfTruth) // experimental knob — the reason for @OptIn } ``` `@OptIn(...)` at the use site is the form to prefer. Kotlin also accepts a module-wide `-opt-in=org.mobilenativefoundation.store6.core.ExperimentalStoreApi` compiler argument, but that blankets every file in the module and hides which call sites actually depend on unstable surface. Keep the acknowledgment next to the code that makes it. ## `@DelicateStoreApi`: stable but easy to misuse Marks API that is **stable but easy to misuse**. This is not a stability warning. The shape will not change out from under you. Opting in asserts something about *you*: that you uphold the documented contract of the marked declaration. The compiler says: > This is a delicate Store API. Read the contract documentation of the declaration before opting > in. Implementations must uphold every documented semantic. The canonical case is implementing `Store` directly instead of building one through the `store { }` DSL: ```kotlin @SubclassOptInRequired(DelicateStoreApi::class) public interface Store ``` `@SubclassOptInRequired` gates only implementation, not use. Calling `stream` or `get` on a store you built through the DSL requires no opt-in at all. Writing `class MyStore : Store` does. Implementing the interface yourself is a deliberate act, not a default, and what the act declares is that your implementation honors every semantic in the interface's KDoc, including the one-failure-channel rule described in [the read contract](https://store.mobilenativefoundation.org/llms/store6/concepts/read-contract.md). The seam interfaces you implement carry the same subclass gate. `FreshnessValidator`, for example, is `@ExperimentalStoreApi` on the type and `@SubclassOptInRequired(DelicateStoreApi::class)` on implementation. Plugging in your own seam implementation is therefore a double opt-in: experimental because the seam is not frozen yet, delicate because the contract is yours to uphold. ## `@InternalStoreApi`: never yours Marks API that is **internal to the Store libraries**. It may change or disappear without notice even in patch releases, and it must never be used outside `org.mobilenativefoundation.store` artifacts. The compiler says: > This API is internal to Store and must not be used outside Store artifacts. There is no supported reason to opt into this one. If you find yourself wanting an internal type, usually to change how fetching, persistence, freshness, or observation works, the supported route is the seam: the extension surface exists precisely so that nobody needs the internals. The [extending guide](https://store.mobilenativefoundation.org/llms/store6/guides/extending.md) covers that seam. ## The tier is on the artifact **Experimental code lives in separate artifacts, never annotation-gated inside a stable one.** When a capability needs its own release rhythm, it gets its own artifact, and the tier is stated on the artifact you depend on rather than buried in an annotation on a member you have already built against. The artifact placement scopes the compatibility promise. **SemVer applies to the stable tier.** A breaking change to an `@ExperimentalStoreApi` surface in a minor release is not a SemVer violation, because that surface never claimed the guarantee. As of 6.0.0-alpha01 the split is: * **`store6-core`**: stable-track. The API is **not frozen** until the beta01 freeze candidate. * **Everything else with an API surface** (`store6-testing`, the `store6-sqldelight`, `store6-room`, and `store6-compose` adapters, `store6-mutations`, and the devtools artifacts) is experimental (`@ExperimentalStoreApi`). `store6-mutations` is an entire experimental artifact: every public symbol carries the marker, and its graduation criteria are [stated in the stability policy](https://store.mobilenativefoundation.org/docs/store6/stability#mutations). The [mutations overview](https://store.mobilenativefoundation.org/llms/store6/mutations.md) introduces its journalled write model. The [stability policy](https://store.mobilenativefoundation.org/llms/store6/stability.md) carries the full artifact table, release targets, and the deprecation cycle. It is the synchronized authority and this page deliberately does not duplicate it. ## The seam: freeze candidate, not frozen Inside `store6-core`, the `org.mobilenativefoundation.store6.core.seam` package (the 13 files you implement to plug in your own fetcher, source of truth, bookkeeper, clock, telemetry, or overlay) is a **freeze candidate, not frozen**. Today those types are `@ExperimentalStoreApi`, so implementing one is an explicit opt-in. This is the stated exception to the separate-artifacts rule above, and it is why the seam sits inside a stable-track artifact rather than shipping separately. The candidate-versus-frozen distinction is deliberate and staged. A real producer has to exercise a seam end to end before it is called a candidate. The `Overlay` and `StoreWriteHandle` surfaces become frozen only once the ack-path atomicity work and its test matrix are green. If that work misses beta01, those two ship `@ExperimentalStoreApi` outside the frozen tier and the rest of core freezes on schedule. The freeze timeline lives on the [roadmap](https://store.mobilenativefoundation.org/llms/store6/roadmap.md). CI enforces the 13-file list on every pull request, so the seam cannot grow quietly. `FetcherResult` is the one seam type that carries no experimental marker. The stable `fetcherOfResult` builder member takes a lambda returning `FetcherResult`, and a stable signature cannot require an experimental type, so `FetcherResult` is the one seam type reachable without any opt-in. ## What opting in commits you to **`@ExperimentalStoreApi`**: expect shapes to change between alphas, and pin your versions accordingly. An experimental surface changing under you in a minor release is the deal you accepted, not a bug report. **`@DelicateStoreApi`**: read the contract KDoc of the declaration you are opting into, and uphold every documented semantic. The KDoc on these declarations is the contract, not commentary. Custom source-of-truth and bookkeeper implementations should run the contract kits that ship in `store6-testing` (`SourceOfTruthContractKit`, `BookkeeperContractKit`). The [testing guide](https://store.mobilenativefoundation.org/llms/store6/guides/testing.md) and [extending guide](https://store.mobilenativefoundation.org/llms/store6/guides/extending.md) cover that workflow. *** Source recorded 2026-08-12 · [`main@539614c0`](https://github.com/matt-ramotar/Store6/commit/539614c06be1a8f20dead562585e47394551ebae) · pre-6.0.0-alpha01 --- # Errors and failure handling Canonical page: https\://store.mobilenativefoundation.org/docs/store6/concepts/errors Markdown: https\://store.mobilenativefoundation.org/llms/store6/concepts/errors.md Source kind: site-authored; source path: content/docs/store6/concepts/errors.mdx The six StoreError variants, the StoreException get path, rendering served-stale failures, and why retries belong in your fetcher. Store has one failure vocabulary and two delivery channels. On the observation path, `stream` never throws: failures arrive in the flow as `StoreResult.Error` values carrying a structured `StoreError`. On the value path, `get` throws a `StoreException` wrapping the same `StoreError`. That is the one-failure-channel rule of [the read contract](https://store.mobilenativefoundation.org/llms/store6/concepts/read-contract.md). `stream` emits and never throws. `get` throws and never emits. The maintenance operations (`invalidate`, `clear`, and their namespace and global forms) use the same exception for their own persistence failures. One vocabulary to learn, delivered the way each call shape demands. ## The six variants, and no seventh `StoreError` is a sealed class with exactly six variants: | Variant | When you see it | | ------------------------ | ----------------------------------------------------------------- | | `Fetch` | The configured fetcher failed to produce a value. | | `Persistence` | A persistence operation against the store's durable state failed. | | `Conversion` | A value could not be converted between representations. | | `FreshnessUnsatisfiable` | The requested freshness policy could not be satisfied. | | `Conflict` | A write conflicted with authoritative server state. | | `Missing` | No value exists for the key and none could be produced. | The variant set is frozen for the 6.x major. New failure kinds map into these categories through their structured detail payloads rather than as new subclasses, which lets the hierarchy bridge to an exhaustive Swift enum. An exhaustive `when` over `StoreError` is safe to write, with no `else` branch, on Kotlin and on the bridged Swift side. (The [Swift bridge guide](https://store.mobilenativefoundation.org/llms/store6/guides/swift.md) covers that side.) ```kotlin users.stream(UserKey("42")).collect { result -> when (result) { is StoreResult.Loading -> showSpinner() is StoreResult.Data -> render(result.value) is StoreResult.Revalidated -> markFresh(result.age) is StoreResult.Error -> when (val error = result.error) { is StoreError.Fetch -> showRetry(error.message) is StoreError.Persistence -> showRetry(error.message) is StoreError.Conversion -> reportBug(error.message, error.cause) is StoreError.FreshnessUnsatisfiable -> showRetry(error.message) is StoreError.Conflict -> showConflict(error.message, error.serverMeta) is StoreError.Missing -> showEmpty(error.key) } } } ``` The compiler verifies both matches cover every case, and the frozen variant set means a 6.x upgrade will not break them. One stream nuance to know before writing that collect: an `Error` frame terminates the observed flow only in the `Freshness.MustBeFresh` initial cycle. Every other failure leaves the flow live, and later results, including a successful refetch after invalidation, keep arriving. See [Freshness policies](https://store.mobilenativefoundation.org/llms/store6/concepts/freshness.md). ## The message contract Every `StoreError` message states three things: what was attempted, for which key or namespace, and the likely fix. That contract is written into the API documentation of the hierarchy itself, so a message that reaches your logs is designed to be actionable without reproducing the failure. Two variants carry structure beyond the message: * `Conflict.serverMeta` is the server-side `StoreMeta` describing the conflicting state, when the server provided one, and `null` otherwise. * `Missing.key` is the `StoreKey` for which no value exists. Match on those properties rather than parsing message text. The messages are for humans. The structure is for code. ## The get path: StoreException `StoreException` is a `RuntimeException` with two guarantees: its message is the wrapped error's message, and its `error` property exposes the structured `StoreError`. Catch it, match on `.error`, and never parse strings. ```kotlin try { render(users.get(UserKey("42"), Freshness.MustBeFresh)) } catch (e: StoreException) { when (val error = e.error) { is StoreError.Missing -> showEmpty(error.key) else -> showRetry(e.message) } } ``` The method contract on `get` names the cases in which it throws `StoreException`: fetching or source-of-truth access failed, a concurrent `clear` removed the key while its fetch was in flight (its waiters observe `StoreError.Missing`), `Freshness.LocalOnly` found no local value, or the server reported deletion (`StoreError.Missing`). One boundary deliberately sits outside this vocabulary: calling any operation on a closed store fails with `IllegalStateException` and the message `Store is closed.`, not a `StoreError`. A closed store is a lifecycle bug in the caller, not a data failure, and Store keeps the two categories separate. See [Memory, eviction, and store lifecycle](https://store.mobilenativefoundation.org/llms/store6/concepts/memory-and-lifecycle.md). ## Where Missing comes from `StoreError.Missing` has four producers, each pinned by the API contracts: 1. **`Freshness.LocalOnly` with nothing local.** `LocalOnly` never invokes the fetcher. When no value is resident and the source-of-truth probe finds nothing, the read fails `Missing`, without a `Loading` frame and without a fetcher call. 2. **A `clear` racing an in-flight fetch.** After `clear(key)` returns, an in-flight fetch that started before the clear can no longer commit. Its waiters observe `Missing`. 3. **A fetcher returning `FetcherResult.Deleted`.** A destructive remote deletion: the resident value is cleared, its freshness forgotten, streams and waiters receive `Missing`, and the deletion does not trigger an automatic refetch. 4. **An expert validator returning `Skip` with no resident value.** If you install a custom `FreshnessValidator` through the seam (an experimental surface), a `FetchPlan.Skip` decision with no resident value yields `Missing`: `get` throws, `stream` emits `Error`. The first is a policy choice, the second a race resolved conservatively, the third a server statement of fact, and the fourth an expert configuration. In every case the meaning is the same: no value exists and none could be produced. ## Rendering `servedStale` errors `StoreResult.Error` carries a second field, `servedStale`. It is `true` when an invalidated resident value was served and its refresh failed under `Freshness.CachedOrFetch` or `Freshness.StaleIfError`. It is `false` when no resident was served or the policy withheld it. `servedStale = true` means your user still has usable content on screen. Render an error affordance over that content (a banner, a toast, a retry chip). Do not blank the screen. The stale value was served precisely so you would not have to. ```kotlin // The Error branch of the collect above: is StoreResult.Error -> { if (result.servedStale) { // The last-good value is still on screen. Layer the failure over it. showErrorBanner(result.error) } else { // Nothing was served alongside this failure. showErrorState(result.error) } } ``` This is the visible half of the stale-while-revalidate asymmetry: after `invalidate`, a failed refresh leaves the old content plus an error. After `clear`, it leaves an empty screen plus an error, because you declared the old value unsafe to show. The full decision guide is [Invalidate or clear](https://store.mobilenativefoundation.org/llms/store6/invalidate-vs-clear.md). ## Retries are yours, deliberately The engine does not retry your fetcher. Zero retries, zero backoff, at zero configuration. One demand cycle invokes the fetcher exactly once. A failure schedules no background retry. A later call is new demand, not a continuation of the failed one. [Important defaults](https://store.mobilenativefoundation.org/llms/store6/important-defaults.md) names the conformance test that pins all three of those statements. If you want retries (backoff, jitter, a circuit breaker), build them inside your fetcher, where you control the policy and can see the transport-level failure that Store deliberately does not interpret. From the engine's perspective a retrying fetcher is still exactly one fetch per demand cycle, so deduplication and single-flighting are unaffected. The [fetchers guide](https://store.mobilenativefoundation.org/llms/store6/guides/fetchers.md) collects retry patterns. ## Constructing errors in tests and extensions > **Note** > > The constructors of `StoreError`, `StoreResult`, and `StoreException` are `internal`. The > sanctioned construction door is the seam's `StoreResults` object, marked `@ExperimentalStoreApi`. > For tests, `store6-testing` wraps the same door in `TestStoreResults` with test-friendly > defaults. See [API tiers](https://store.mobilenativefoundation.org/llms/store6/concepts/api-tiers.md) for what the experimental marker > commits to. ```kotlin val error = TestStoreResults.fetchError( message = "GET /users/42 failed: HTTP 503. Check connectivity and retry.", cause = Exception("HTTP 503"), ) val result = TestStoreResults.error(error, servedStale = false) ``` Both objects cover every `StoreResult` state and every `StoreError` variant: `loading()`, `data(...)`, `revalidated(...)`, `error(...)`, and `exception(...)`, plus one factory per error variant (`fetchError`, `persistenceError`, `conversionError`, `freshnessUnsatisfiable`, `conflict`, and `missing`). The [testing guide](https://store.mobilenativefoundation.org/llms/store6/guides/testing.md) shows them at work alongside the fakes and contract kits. *** Source recorded 2026-08-10 · [`main@be470620`](https://github.com/matt-ramotar/Store6/commit/be47062070eba8f8a327279e9c5a68caa0ef06ca) · pre-6.0.0-alpha01 --- # Freshness policies Canonical page: https\://store.mobilenativefoundation.org/docs/store6/concepts/freshness Markdown: https\://store.mobilenativefoundation.org/llms/store6/concepts/freshness.md Source kind: site-authored; source path: content/docs/store6/concepts/freshness.mdx The five per-call Freshness policies, exactly what triggers a fetch, stale-while-revalidate, typed StoreMeta, and the expert read-planning seam. Freshness is a **per-call parameter**, not store-level configuration. Every `stream` and every `get` takes a `Freshness` policy describing how fresh a value must be before it is served, and each read is planned from resident availability, invalidation state, typed metadata, and that policy. Both `stream` and `get` default to `Freshness.CachedOrFetch`. Because the policy travels with the call, two screens can read the same key with different demands (a list view happy with anything resident, a checkout flow that insists on a fresh fetch) without configuring two stores. Concurrent requests for one key still share a single in-flight fetch even when their policies differ. ## The five policies `Freshness` is a sealed interface with exactly five variants. No sixth fetching mode exists in the API. ### `CachedOrFetch`: the default Serve a locally available value immediately. Invalidated values and source-of-truth rows without freshness metadata are served as stale while one background revalidation runs. Fetch when no local value exists. This is stale-while-revalidate, and it is the default shape of every read. More on that below. ### `MaxAge(notOlderThan)` Serve a locally available value only when it has known freshness metadata, has not been invalidated, and its age does not exceed `notOlderThan`. Otherwise withhold it and fetch a fresh value. A row hydrated by an external writer with no recorded freshness metadata is **withheld** under `MaxAge`. The read fetches rather than serving it. If you want a metadata-less row served while it refreshes, that is `CachedOrFetch`'s job. ### `MustBeFresh` Never serve a cached value. Block until a fresh fetch succeeds and fail when it does not. A source-of-truth row without freshness metadata is also withheld. `MustBeFresh` is the one policy whose initial-cycle failure terminates a stream: an initial-cycle fetch or revalidation failure emits one error and completes the flow, where every other failure leaves the flow live. The full rules for which failures end a stream are on the [read contract](https://store.mobilenativefoundation.org/llms/store6/concepts/read-contract.md) page. ### `StaleIfError` Prefer fresh data after invalidation or when a local value has no freshness metadata, but fall back to that stale value when the fetch fails. A local value with current known metadata is served without fetching. On `get`, the difference from the default is blocking behavior: `StaleIfError` blocks after invalidation and returns the resident value only when the refresh fails, whereas `CachedOrFetch` returns the resident value immediately and refreshes it in the background. ### `LocalOnly` Never invoke the fetcher. Serve only locally available data and report `StoreError.Missing` when none exists. On a memory miss, the configured source of truth is probed once, so a pre-existing persisted row counts as locally available data. When nothing is resident anywhere, the read fails `Missing` without a `Loading` frame and without calling the fetcher. Pitfall: the builder still requires a fetcher even if every read you ever issue is `LocalOnly`. `store { }` without one fails at build time with a message that says so. `LocalOnly` changes what a read does, not what a store needs. The policy goes at the call site: **Per-call freshness** ```kotlin val profile = users.get(UserKey("1"), Freshness.MaxAge(5.minutes)) users.stream(UserKey("1"), Freshness.LocalOnly).collect { result -> render(result) } ``` ## What triggers a fetch, and what does not | Read | Fetches? | | ------------------------------------------------------------------------------ | ------------------------------------------------------- | | Absent key, under any policy except `LocalOnly` | Yes | | `MustBeFresh`, even against a fresh resident value | Always | | `MaxAge` with the resident value over its bound, invalidated, or metadata-less | Yes | | `StaleIfError` after invalidation, or with a metadata-less local value | Yes. The stale value is served only if the fetch fails. | | Stale resident value under `CachedOrFetch` | Yes, in the background, after serving the stale value | | Fresh resident value under `CachedOrFetch` | No | | `MaxAge` with the resident value within its bound | No | | `StaleIfError` with a local value whose metadata is current | No | | `LocalOnly` | Never | The default behaviors in this table are each pinned by a named conformance test on [Important defaults](https://store.mobilenativefoundation.org/llms/store6/important-defaults.md). That page is the specification of record for defaults, and the remaining rows restate the documented `Freshness` contract. If this table and those tests ever disagree, the tests are right. ## Stale-while-revalidate is the default shape Under `CachedOrFetch`, a stale resident value is served immediately and one refresh runs. That refresh produces **exactly one** terminal outcome (one fresh `Data`, or one served-stale `Error`, or one `Revalidated`), never two. This shape degrades gracefully offline: a user with a stale cache and no network sees data, then one error frame, rather than a spinner. It is also why `invalidate` is the safe primitive for pull-to-refresh. The resident value keeps rendering while the refetch runs. The full comparison is on [Invalidate or clear](https://store.mobilenativefoundation.org/llms/store6/invalidate-vs-clear.md). The refresh does *not* retry. Zero retries, zero backoff: one demand cycle invokes the fetcher exactly once, a failure schedules nothing in the background, and a later call is new demand rather than a continuation of the failed one. If you want retries, they belong in your fetcher, where you control the policy. How failures surface is covered on the [errors](https://store.mobilenativefoundation.org/llms/store6/concepts/errors.md) page. Writing fetchers gets its own guide. ## One fetch, many policies N concurrent readers of one key share one fetch. Fifty getters and fifty collectors demanding the same key produce exactly one fetch, and all one hundred observe its outcome. A stream that arrives while a fetch is in flight piggybacks on it rather than starting a second one, and this holds even when the callers passed different `Freshness` policies. Cancelling one waiter does not cancel the shared fetch. The work commits, and the next caller reuses it. ## Typed metadata: StoreMeta The freshness and identity metadata behind all of this is typed. An untyped metadata channel does not exist anywhere in Store. ```kotlin public interface StoreMeta { /** The wall-clock time at which the value was written, in Unix epoch milliseconds. */ public val writtenAtEpochMillis: Long /** The optional entity tag associated with the value. */ public val etag: String? } ``` `StoreResult.Data.age` and the age-bounded policies derive from this metadata. Milliseconds since the Unix epoch are used because no stable cross-platform instant type exists on the current language floor. The rule that ties the policies together is **conservative staleness**: a resident value with null metadata is treated as conservatively stale by read planning. That single rule is why `CachedOrFetch` serves a metadata-less row and revalidates it in the background, while `MaxAge` and `MustBeFresh` (whose contracts require *known* freshness) withhold it and fetch. ## Advanced: the read-planning seam > **Warning** > > **Experimental.** Everything in this section is `@ExperimentalStoreApi` and lives in the > `seam` package, which is a freeze candidate, not frozen. Implementing a seam interface is an > explicit opt-in, and shapes can change in any release. See > [the stability policy](https://store.mobilenativefoundation.org/llms/store6/stability.md) for what candidate-versus-frozen means. You will probably never touch this seam. The five policies absorb the cases that needed custom validation logic in earlier Store versions. It exists for the rare read-planning decision the policies cannot express. A `FreshnessValidator` selects a fetch plan as a pure function of one coherent `FreshnessContext`: resident availability, the resident value's recorded `StoreMeta` (or null), epoch staleness, the read's `Freshness` policy, the wall-clock reading captured for the plan, and the durable bookkeeping posture: ```kotlin @ExperimentalStoreApi @SubclassOptInRequired(DelicateStoreApi::class) public interface FreshnessValidator { /** Plans whether and how the current read should fetch as a pure function of [context]. */ public fun plan(context: FreshnessContext): FetchPlan } ``` `FetchPlan` has three outcomes: * **`Skip`**: no fetch. Skip with no resident value yields `StoreError.Missing` (`get` throws, `stream` emits `Error`). * **`Fetch(servesResidentWhileFetching)`**: an unconditional fetch, optionally serving the resident value while it runs. * **`Conditional(etag, servesResidentWhileFetching)`**: a conditional fetch for the recorded ETag. A `Conditional` plan is how ETags reach a seam fetcher: the fetcher receives the ETag the plan selected, and a `FetcherResult.NotModified` response comes back to collectors as exactly one `Revalidated` frame, with the resident value's age, never a redundant `Data` frame, and clears staleness. Deep coverage of conditional fetching and custom validators belongs to the fetchers [guide](https://store.mobilenativefoundation.org/llms/store6/guides/fetchers.md) and the [extending-Store guide](https://store.mobilenativefoundation.org/llms/store6/guides/extending.md). ## Coming from Store 5 Store 5 asked you to supply a [Validator](https://store.mobilenativefoundation.org/docs/concepts/store5/validator), a hook the store called to decide whether cached data was still good, defaulting to "always valid" when absent. Store 6 absorbs that job natively: per-call policies say how fresh each read must be, durable invalidation marks data stale from the write side, and the `FreshnessValidator` seam remains for the rare custom case. | Store 5 | Store 6 | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `Validator` supplied at build time, one answer per store | Per-call `Freshness` policies + durable `invalidate`, with the experimental `FreshnessValidator` seam for custom read planning | Use [Migrating from Store 5](https://store.mobilenativefoundation.org/llms/store6/migration/from-store5.md) for the full path and the [component map](https://store.mobilenativefoundation.org/llms/store6/migration/component-map.md) for component-by-component translation. *** Source recorded 2026-08-12 · [`main@c67a94ed`](https://github.com/matt-ramotar/Store6/commit/c67a94ed30460a35161c2cbc3e725f127caf055e) · pre-6.0.0-alpha01 --- # Memory, eviction, and store lifecycle Canonical page: https\://store.mobilenativefoundation.org/docs/store6/concepts/memory-and-lifecycle Markdown: https\://store.mobilenativefoundation.org/llms/store6/concepts/memory-and-lifecycle.md Source kind: site-authored; source path: content/docs/store6/concepts/memory-and-lifecycle.mdx What keeps a per-key engine alive, how maxIdleKeys bounds the idle ones, and why eviction never changes what a read returns. A store keeps one engine per active key. That engine tracks demand, runs the fetch pipeline, and feeds collectors for the key. Active work keeps an engine resident, the idle-key limit bounds idle engines, eviction preserves read behavior, and closing ends the store's lifecycle. ## What pins an engine Engines whose key has active collectors, in-flight work, or an in-flight fetch are always resident and are never evicted. Eviction considers only quiescent engines: keys nobody is currently watching or fetching. This holds under churn: cycling demand across many keys never evicts a held engine, and total residency stays bounded while it happens. Like the other zero-config memory guarantees on this page, it is pinned by a named conformance test. [Important defaults](https://store.mobilenativefoundation.org/llms/store6/important-defaults.md) lists the test behind each one, and if this page and a test ever disagree, the test is right. ## Bounding idle keys: maxIdleKeys Once a key becomes quiescent its engine parks in an LRU idle set holding at most `count` engines. The eldest quiescent engine beyond the bound is destroyed. The default is 128, and the zero-config cap behaves identically to an explicit `maxIdleKeys(128)`. Passing `0` destroys every engine at quiescence. The count must be `>= 0`. A negative count fails at configuration time with `IllegalArgumentException`. ```kotlin val users = store { fetcher { key -> api.getUser(key.id) } maxIdleKeys(256) } ``` `maxIdleKeys` is the one stable builder knob other than the fetcher itself. It needs no experimental opt-in. Every other configuration point on the builder is an `@ExperimentalStoreApi` seam install (see the note below). ## Eviction is semantically invisible Eviction discards only derived in-memory state. Durable rows, freshness metadata, stale marks, and invalidation watermarks live in the source of truth and the bookkeeper, not in the engine, so a later read of an evicted key is semantically identical to one that was never evicted. Destroying and recreating an engine preserves per-key stale marks and namespace watermarks, and still drives the refetch you would have gotten. Invalidation watermarks go further: they survive restart as well as eviction, and a namespace or global invalidation is observed even by a key a fresh store has never seen. Two consequences follow: * **`maxIdleKeys(0)` does not disable caching.** It destroys every engine the moment its key goes quiescent, but the value still lives in the source of truth and its freshness record in the bookkeeper. The next read rebuilds an engine over the same truth. * **Eviction does not lose invalidation state.** A stale mark or watermark is durable truth, not engine state. Evicting a key you just invalidated changes nothing about what happens when you read it next. ## What "durable" means with zero config The defaults are an in-memory source of truth and an in-memory bookkeeper. Nothing is written to disk until you install `persistence(...)`. So with zero configuration, "durable" means: outside the engine. That state survives eviction, which is why eviction is invisible, but it does not survive process death. To keep values and freshness bookkeeping across restarts, install a [persistence adapter](https://store.mobilenativefoundation.org/llms/store6/guides/persistence.md). [store6-sqldelight](https://store.mobilenativefoundation.org/llms/store6/sqldelight.md) is a complete one, and a [Room adapter](https://store.mobilenativefoundation.org/llms/store6/room.md) ships alongside it. > **Note** > > `persistence`, `bookkeeper`, `telemetry`, `overlay`, `wallClock`, and `freshnessValidator` are > `@ExperimentalStoreApi` seam installs. The zero-config path (a fetcher plus `maxIdleKeys`) needs > no opt-in. [API tiers](https://store.mobilenativefoundation.org/llms/store6/concepts/api-tiers.md) explains what the experimental tier does > and does not promise. ## Single-flight deduplication Concurrent demand for one key shares one fetch. Fifty getters and fifty collectors demanding the same key produce exactly one fetch, and all one hundred observe its outcome. A stream that arrives while a fetch is in flight piggybacks on it rather than starting a second one. Cancelling a waiter does not cancel the shared fetch. The work commits anyway, and the next caller reuses the committed value instead of refetching. This is also one of the pins from the first section: an in-flight fetch keeps its engine resident until the work resolves, so eviction can never strand a fetch that callers are waiting on. ## Reader grace When the last collector for a key leaves, the pipeline does not tear down instantly. A re-subscription within a short window resumes the existing pipeline rather than starting over with a fresh `Loading` frame, so a recomposition or a quick navigation round-trip does not flash a loading state over a value the store already has. The window's millisecond value is an internal constant, and it is deliberately not documented or test-pinned. It is not contractual: do not design against a specific number, and do not write tests that depend on one. If your UI collects through a lifecycle-aware entry point (see [store6-compose](https://store.mobilenativefoundation.org/llms/store6/compose.md) for lifecycle-gated collection), grace is what absorbs the gap between stop and restart. ## The end of the store's life: close() `close()` releases the resources the store owns and cancels its in-flight work. Collectors and value requests waiting on in-flight work are cancelled. Every subsequent operation fails with `IllegalStateException` and the exact message `Store is closed.` Calling `close()` more than once has no additional effect. Scope a store to a lifecycle you own (an application, a user session, a dependency-injection scope) and close it when that lifecycle ends. Nothing else ends it for you: a `stream` flow remains active until its collector is cancelled or the store is closed, as the [read contract](https://store.mobilenativefoundation.org/llms/store6/concepts/read-contract.md) spells out. If a store you expected to be long-lived starts throwing `IllegalStateException`, something closed it early. If streams outlive the screen that started them, something is collecting outside the lifecycle it should be scoped to. ## Where to go next * [Important defaults](https://store.mobilenativefoundation.org/llms/store6/important-defaults.md): the test-pinned specification of every zero-config decision this page describes. * [The read contract](https://store.mobilenativefoundation.org/llms/store6/concepts/read-contract.md): what `stream` and `get` promise, including the failure channels `close()` cuts off. * [store6-compose](https://store.mobilenativefoundation.org/llms/store6/compose.md): lifecycle-gated collection in Compose UIs. * [store6-sqldelight](https://store.mobilenativefoundation.org/llms/store6/sqldelight.md): persistence that survives process death. *** Source recorded 2026-08-10 · [`main@be470620`](https://github.com/matt-ramotar/Store6/commit/be47062070eba8f8a327279e9c5a68caa0ef06ca) · pre-6.0.0-alpha01 --- # The read contract: stream, get, and origins Canonical page: https\://store.mobilenativefoundation.org/docs/store6/concepts/read-contract Markdown: https\://store.mobilenativefoundation.org/llms/store6/concepts/read-contract.md Source kind: site-authored; source path: content/docs/store6/concepts/read-contract.mdx One failure rule, four result kinds, honest origin attribution, and the lifecycle of a Store stream. A Store has exactly two read operations (`stream` and `get`) and one failure rule: `stream` emits failures as values and never throws them at your collector. `get` returns a value or throws, and never emits partial states. Everything on this page is the contract of the `Store` interface in `store6-core`. It holds for every store you build, from a five-line memory-only store to one backed by persistence and optimistic writes. ```kotlin public fun stream(key: K, freshness: Freshness = Freshness.CachedOrFetch): Flow> public suspend fun get(key: K, freshness: Freshness = Freshness.CachedOrFetch): V ``` > **Note** > > `store6-core` is stable-track, but the API is **not frozen** until the beta01 freeze candidate. > Nothing on this page needs an experimental opt-in. `stream`, `get`, `close`, `StoreResult`, and > `Origin` are plain public API. Implementing `Store` yourself is different: the interface requires a > `DelicateStoreApi` opt-in to subclass, because an implementation must uphold every semantic this > page describes. ## The one-failure-channel rule **`stream` never throws retrieval failures.** Fetcher and source-of-truth failures encountered while retrieving a key are emitted as `StoreResult.Error` values rather than thrown to the collector. Your collector sees the failure as data, decides how to render it, and (with one exception covered below) keeps collecting a live flow. **`get` never emits.** It returns the resolved value or throws `StoreException`. There is no partial state on this path: `get` throws when fetching or source-of-truth access failed, when a concurrent `clear` removed the key while its fetch was in flight, when `Freshness.LocalOnly` found no local value, or when the server reported deletion. The one exception on both doors is not a retrieval failure at all: any operation on a closed store throws `IllegalStateException` with the message `Store is closed.` That is a usage error in your code (you kept a reference past `close()`), not something a fetcher did. The structured error vocabulary behind `StoreResult.Error` and `StoreException` has its own page: [Errors and failure handling](https://store.mobilenativefoundation.org/llms/store6/concepts/errors.md). ## The four result kinds `StoreResult` is a sealed interface with exactly four kinds. There is no fifth case, so an exhaustive `when` is safe to write and will stay safe: * **`Loading`**: the store has no servable resident value under the policy in effect. This is policy-relative, not merely "empty cache": a resident value that `Freshness.MustBeFresh` withholds produces `Loading` even though bytes are sitting in memory. * **`Data(value, origin, age, isStale, refreshing)`**: a value, with the source it came from, the elapsed time since it was committed, whether it was invalidated or exceeds the age bound of the freshness policy in effect, and whether a fetch was in flight for the key when the result was emitted. * **`Revalidated(age)`**: the not-modified signal of a conditional fetch. The server confirmed the current value is still fresh, metadata was refreshed, and `age` is the elapsed time since the last commit, measured at revalidation. No new `Data` frame is produced, because no new value exists. * **`Error(error, servedStale)`**: a retrieval failure, carrying a structured `StoreError` and whether a stale value was served alongside it. Collectors that only handle `Loading`/`Data`/`Error` silently miss the "nothing changed" signal. After a conditional fetch returns not-modified, the store emits one `Revalidated`, not a redundant `Data`. Handle all four. This is the exhaustive `when` from the quickstart program that compiles and runs in CI: ```kotlin users.stream(UserKey("1")).take(2).collect { result -> when (result) { is StoreResult.Loading -> println("Loading…") is StoreResult.Data -> println("Data(name=${result.value.name}, origin=${result.origin})") is StoreResult.Revalidated -> println("Revalidated(age=${result.age})") is StoreResult.Error -> println("Error(${result.error})") } } ``` ## Origins: attribution is honest Every `Data` frame names the source it came from. `Origin` is an enum with four values: | Origin | Meaning | | --------- | --------------------------------------------------------------- | | `MEMORY` | The value was served from in-memory resident state. | | `SOT` | The value was read from the store's persistent source of truth. | | `FETCHER` | The value was produced by the store's configured fetcher. | | `OVERLAY` | The value reflects an overlay applied above stored data. | Attribution honesty is a tested contract, not a debugging aid: a network commit is attributed `FETCHER`, an external durable change is attributed `SOT`, an optimistic write is attributed `OVERLAY`, and each of those claims is pinned by a named conformance test on [Important defaults](https://store.mobilenativefoundation.org/llms/store6/important-defaults.md). If this page and a test ever disagreed, the test would be right and this page would be a bug. Two rules govern `OVERLAY` frames on the write path: * **Overlay frames are fresh by definition.** They are stamped `age = Duration.ZERO` and `isStale = false` unconditionally, because an optimistic value genuinely is new. The user just wrote it. Only `refreshing` is live on an overlay frame. A pending-write affordance therefore keys on `origin == Origin.OVERLAY`, never on `isStale`. A spinner driven by `isStale` will never fire for a pending write, and that is intended. The full consumer guidance lives in [the stability policy](https://store.mobilenativefoundation.org/docs/store6/stability#9-reading-pending-writes-and-staleness), and the [pending-write UI guide](https://store.mobilenativefoundation.org/llms/store6/mutations/pending-write-ui.md) covers the consumer pattern in depth. * **`get` is never projected by a configured overlay.** Overlays apply only to `stream`, so an optimistic write is invisible to `get`. It is a point read of committed truth. To observe your own optimistic write, observe `stream`. ## Conflation: lifecycle signals are never dropped A slow collector never blocks a fast one, and never blocks the engine. When a collector falls behind, the store conflates, but conflation is **per result kind**: * A newer `Data` supersedes an older queued `Data`. The slow collector skips intermediate values and observes the latest row. * A queued `Loading`, `Error`, or `Revalidated` is **never displaced by another kind**. Lifecycle signals survive backpressure. Only a newer signal of the same kind can supersede an older queued one, so the kind itself is never lost. In UI code, you may not see every intermediate value, but you will never miss the fact that a load started, a fetch failed, or the server confirmed freshness, and every collector eventually observes the latest row. Each of these claims is pinned by a named conformance test on [Important defaults](https://store.mobilenativefoundation.org/llms/store6/important-defaults.md). ## Errors on the stream: servedStale `StoreResult.Error` carries `servedStale`, and its meaning is exact: `true` when an invalidated resident value was served and its refresh failed under `Freshness.CachedOrFetch` or `Freshness.StaleIfError`. `false` when no resident was served or the policy withheld it. The rendering rule follows directly. When `servedStale` is `true`, the user is still looking at usable content. The stale value was emitted before the failure arrived. Show an error affordance over that content. Do not blank the screen. When `servedStale` is `false`, there is nothing on screen to preserve, and a full error state is honest. The `StoreError` vocabulary that tells you *what* failed is covered in [Errors and failure handling](https://store.mobilenativefoundation.org/llms/store6/concepts/errors.md). ## Failure trace: invalidated persisted data This trace has four qualifying conditions: a first cold subscription after restart, a durably invalidated persisted row, the default freshness validator, and `Freshness.CachedOrFetch`. Wall-clock age alone does not trigger this fetch. A custom `FreshnessValidator` can plan differently. 1. Store hydrates the persisted row into resident state and requests an unconditional fetch with `etag=null`. The durable record may still retain its stored ETag. 2. The initial public result is `Data(origin=Origin.SOT, isStale=true, refreshing=true)`. 3. A queued stale `Data` replay may occur before the error, without an intervening `Loading`. 4. `Bookkeeper.recordFailure` completes before the public error is emitted. 5. The failed refresh produces `Error(StoreError.Fetch, servedStale=true)`, and the stream remains live. After hydration populates memory, a later resident replay may carry `Origin.MEMORY`. ## The one stream-terminating case Exactly one failure completes a stream: a `Freshness.MustBeFresh` initial-cycle fetch or revalidation failure emits one `Error` and completes the flow. `MustBeFresh` promises never to serve residence, so when the fresh fetch fails there is nothing left for the stream to do. Every other failure, under every other policy, leaves the flow live: the error arrives as a value and collection continues. > **Tip** > > Do not wrap stream collection in `try`/`catch` expecting thrown retrieval errors. Errors arrive as > `StoreResult.Error` values, and the stream usually stays live after one. A `catch` block around > `collect` sees the closed-store `IllegalStateException` and cancellation, never retrieval > failures. Handling failures there means your UI never renders them. ## Streams are unbounded A stream has no natural end. The flow remains active until its collector is cancelled or the store is closed, and it continues to report later values: * **Refetches triggered by invalidation.** After `invalidate(key)`, an active stream observes the refetched data. * **The absent-value transition after a clear.** After `clear(key)`, an active stream observes `Loading` (the value is gone) and then refetched data. What each maintenance verb makes streams observe is the subject of [Invalidate or clear](https://store.mobilenativefoundation.org/llms/store6/invalidate-vs-clear.md). Concurrent collectors and callers for one key share a single fetch. Fifty getters and fifty collectors of one key cost one fetch, and all of them observe its outcome. How sharing, eviction, and collector lifecycle interact is covered in [Memory, eviction, and store lifecycle](https://store.mobilenativefoundation.org/llms/store6/concepts/memory-and-lifecycle.md). ## Closing the store `close()` releases the resources the store owns and cancels its in-flight work. Collectors and value requests waiting on in-flight work are cancelled. Every subsequent call to any operation fails with `IllegalStateException` and the exact message `Store is closed.` Calling `close()` more than once has no additional effect. ## Read resolution at a glance Every data result names the boundary that supplied the value. Origin and freshness are independent: read `origin` to identify provenance, then read `isStale` and `refreshing` for lifecycle state. | Origin | Resolution boundary | Meaning | | ---------------- | ------------------- | ------------------------------------------------------------------------------- | | `Origin.MEMORY` | Resident replay | The collector receives a value already resident in this engine. | | `Origin.SOT` | Source of truth | A source-of-truth read or write supplied the confirmed value. | | `Origin.FETCHER` | Fetcher | The configured fetcher produced or revalidated the authoritative value. | | `Origin.OVERLAY` | Stream projection | An overlay projected over confirmed residence or confirmed absence for streams. | > **Info: Important default** > > With Store 6's default freshness validator, wall-clock age alone never makes` Freshness.CachedOrFetch` fetch. It fetches when no resident value exists, freshness metadata is missing, the resident is invalidated, or durable status marks it stale. Use `Freshness.MaxAge` when elapsed age should participate. A custom` FreshnessValidator` may plan differently, and` Freshness.MustBeFresh` follows different serving and failure rules. With Store 6's default freshness validator and` Freshness.CachedOrFetch`, the first cold stream after restart serves a durably invalidated persisted row as` Data(origin=Origin.SOT, isStale=true, refreshing=true)`. If its refresh fails, the stream emits `Error(StoreError.Fetch, servedStale=true)` without an intervening `Loading`, and the stream stays live. `Bookkeeper.recordFailure` completes before that fetch error is emitted. Hydrated resident metadata does not reuse the persisted ETag, so a fetch planned from that state sees `etag=null`. After the first hydrated emission, a later resident emission may use `Origin.MEMORY`. Read the [read contract](https://store.mobilenativefoundation.org/llms/store6/concepts/read-contract.md) for the complete stream and point-read semantics. Use the [freshness policies](https://store.mobilenativefoundation.org/llms/store6/concepts/freshness.md) to choose when a fetch participates. ## Where next * [Freshness policies](https://store.mobilenativefoundation.org/llms/store6/concepts/freshness.md): how each read is planned, and what does and does not trigger a fetch. * [Errors and failure handling](https://store.mobilenativefoundation.org/llms/store6/concepts/errors.md): the structured `StoreError` vocabulary behind `Error` and `StoreException`. * [Invalidate or clear](https://store.mobilenativefoundation.org/llms/store6/invalidate-vs-clear.md): what streams observe after each maintenance verb. * [Compose integration](https://store.mobilenativefoundation.org/llms/store6/compose.md): collecting this contract in UI. * [Quickstart](https://store.mobilenativefoundation.org/llms/store6/quickstart.md): the runnable program this page's snippet comes from. *** Source recorded 2026-08-12 · [`main@539614c0`](https://github.com/matt-ramotar/Store6/commit/539614c06be1a8f20dead562585e47394551ebae) · pre-6.0.0-alpha01 --- # Contributing to Store Canonical page: https\://store.mobilenativefoundation.org/docs/store6/contributing Markdown: https\://store.mobilenativefoundation.org/llms/store6/contributing.md Source kind: source-synced; source path: CONTRIBUTING.md Source revision: ad435df1095673709a22f1b52a82aa03748cd9b3 Thanks for considering contributing to Store. This document provides guidelines and information about how you can contribute. ## Getting Started * **Fork the Repository**: Start by forking the [MobileNativeFoundation/Store](https://github.com/MobileNativeFoundation/Store) repository. * **Clone the Fork**: Clone your fork to your machine to start working on the changes. ## Contribution Workflow ### Reporting Issues * **Search Existing Issues**: Before creating a new issue, please do a search in existing issues to see if it has been reported or fixed. * **Create a Detailed Issue**: If you find a bug or have a feature request, please create an issue with a clear title and a detailed description. ### Submitting Changes * **Create a Branch**: Create a branch in your fork for your contribution. * **Make Your Changes**: Make your changes and commit them to your branch. Make sure to write clear, concise commit messages. * **Write Tests**: If you are adding new features or fixing bugs, write tests that cover your changes. * **Run the Tests**: Run the project's existing tests to ensure nothing is broken. * **Create a Pull Request**: Submit a PR to the main repository for review. Include a clear description of the changes and any relevant issue numbers. ### Code Review Process * **Wait for Review**: Maintainers will review your PR and might request changes. * **Make Requested Changes**: If changes are requested, make them and update your PR. * **Merge**: Once your PR is approved, a maintainer will merge it into the main codebase. ## Community Guidelines * **Be Respectful**: Treat everyone with respect. We strive to create a welcoming and inclusive environment. * **Follow the Code of Conduct**: Familiarize yourself with our [Code of Conduct](https://github.com/MobileNativeFoundation/Store/blob/main/CODE_OF_CONDUCT.md). ## Getting Help * **Join the Community**: If you have questions or need help, join our [Slack channel](https://kotlinlang.slack.com/archives/C06007Z01HU). --- # store6-graphql Canonical page: https\://store.mobilenativefoundation.org/docs/store6/graphql Markdown: https\://store.mobilenativefoundation.org/llms/store6/graphql.md Source kind: source-synced; source path: store6-graphql/README.md Source revision: ad435df1095673709a22f1b52a82aa03748cd9b3 GraphQL operation fetcher kit for Store v6. A store built with this module is a document cache: each execution of one GraphQL operation, identified by operation name plus canonical variables, caches one decoded response value. Everything here is `@ExperimentalStoreApi`. The seam it consumes is a freeze candidate, not frozen. See [STABILITY.md](https://store.mobilenativefoundation.org/llms/store6/stability.md). The module has no dependency beyond `store6-core` and ships the same Kotlin Multiplatform target set as `store6-core`. You bring the transport: a `GraphQlExecutor` you implement on your HTTP client owns JSON encoding of variables, decoding of response data, and translation of the response `errors` array into `GraphQlError` values. The kit never parses GraphQL documents and never normalizes responses into entities. ## Install Until the snapshot is published remotely, publish `store6-core` and `store6-graphql` to Maven Local: ```shell ./gradlew :store6-core:publishToMavenLocal :store6-graphql:publishToMavenLocal ``` ```kotlin repositories { mavenLocal() mavenCentral() } dependencies { implementation("org.mobilenativefoundation.store:store6-graphql:6.0.0-SNAPSHOT") } ``` ## First result ```kotlin val getUser = GraphQlOperation( document = "query GetUser(\$id: ID!) { user(id: \$id) { id name } }", name = "GetUser", ) val store = store { graphQlFetcher(getUser) { request -> // Your transport: encode request.variables, send request.operation.document, // decode response data to User and response errors to GraphQlError values. val response = api.execute(request.operation.document, request.variables) GraphQlExecutorResult.Data(data = response.user, errors = response.errors) } } val ada = store.get(getUser.key(graphQlVariables { put("id", "1") })) ``` A second `get` of the same key, in any variable insertion order, serves the cached response without executing. Every Store read policy applies unchanged: `Freshness.MustBeFresh` re-executes, `Freshness.LocalOnly` never executes, and `store.invalidate(key)` marks one variable set stale. ## Entry points * `GraphQlOperation(document, name)`: an executable document plus the operation name. The document is opaque to Store and reaches the executor unparsed. * `GraphQlOperation.key(variables)`: the `GraphQlOperationKey` for one execution. * `graphQlVariables { }`: builder for `GraphQlVariables` over the `GraphQlValue` model (`NullValue`, `BooleanValue`, `IntValue`, `FloatValue`, `StringValue`, `ListValue`, `ObjectValue`). * `graphQlFetcher(operation, partialDataPolicy, executor)`: the seam `Fetcher`, also installable through the `StoreBuilder.graphQlFetcher(...)` extension. * `GraphQlExecutor`: `suspend execute(GraphQlRequest): GraphQlExecutorResult`. Throw for transport failure. Return `Data(data, errors, etag)` for any response the server produced, or `NotModified(etag)` to answer a conditional request. * `GraphQlOperationException(operationName, errors)`: the `StoreError.Fetch` cause when a response reports errors the fetcher does not adopt. ## Cache identity `GraphQlOperationKey.canonicalId()` is `()`. The variable rendering is JSON-shaped with no whitespace. Object keys sort in UTF-16 code-unit order at every nesting depth, list order is significant, strings use JSON escaping, and an explicit `null` variable is a different identity than an absent one. Two keys with equal variables are one cache entry regardless of insertion order. `FloatValue` renders through the runtime's `Double.toString`, which differs across Kotlin targets (JS drops the trailing `.0` of whole numbers). Prefer int or string variables when canonical ids must match across runtimes, for example in a shared persistent source of truth. The default namespace is `graphql:`, so `store.invalidateNamespace(key.namespace)` marks every cached variable set of that one operation stale. Pass a custom namespace to `GraphQlOperationKey` to partition differently. ## Response mapping | Executor outcome | Store outcome | | -------------------------------------------------------- | ---------------------------------------------------------------------------- | | `Data(data != null, no errors)` | `FetcherResult.Success(data, etag)` | | `Data(data != null, errors)` + `FailOnErrors` (default) | `FetcherResult.Error(GraphQlOperationException)` | | `Data(data != null, errors)` + `AdoptPartialData` | `FetcherResult.Success(data, etag)` | | `Data(data == null, errors)` | `FetcherResult.Error(GraphQlOperationException)` | | `Data(data == null, no errors)` | `FetcherResult.Error` naming the protocol violation | | `NotModified(etag)` | `FetcherResult.NotModified(etag)`. Streams observe `StoreResult.Revalidated` | | thrown exception | `FetcherResult.Error(cause)`; `CancellationException` propagates | | key whose `operationName` is not the fetcher's operation | `FetcherResult.Error` naming both operations, without executing | `GraphQlPartialDataPolicy.FailOnErrors` is the default so a cached value never contains error-substituted nulls. Opt into `AdoptPartialData` only when the decoded type tolerates missing fields, because the store caches the partial value as if it were complete. ## Conditional requests When the engine plans a conditional fetch for a key with a recorded ETag, `GraphQlRequest.etag` is non-null. An executor that can revalidate (for example HTTP `If-None-Match` on a GET-shaped operation) returns `NotModified` to confirm the cached response. Executors without revalidation support ignore the ETag and execute normally. ## Sample ```shell ./gradlew :store6-graphql-sample:run ``` The headless JVM sample asserts four scenes over an in-process scripted executor: key identity across variable orders, document-cache serve without re-execution, the fail-vs-adopt partial-response split, and `NotModified` revalidation with recorded ETags. --- # Devtools and the inspector Canonical page: https\://store.mobilenativefoundation.org/docs/store6/guides/devtools Markdown: https\://store.mobilenativefoundation.org/llms/store6/guides/devtools.md Source kind: site-authored; source path: content/docs/store6/guides/devtools.mdx `store6-devtools` observes Store's telemetry seam without reading Store internals. It provides a structured logger, an in-memory monitor, and a composite sink. `store6-devtools-inspector` renders the monitor's event-derived state inside a Compose app. There is no external transport or host process. ## Install in one line Opt in to `ExperimentalStoreApi`, import `StoreTelemetryLogger`, and add one line to the store builder: ```kotlin telemetry(StoreTelemetryLogger()) ``` To retain an in-memory projection for the inspector while logging the same events, install both sinks together. This block is verbatim from the devtools README: ```kotlin import org.mobilenativefoundation.store6.devtools.StoreDevtoolsMonitor import org.mobilenativefoundation.store6.devtools.StoreTelemetryLogger import org.mobilenativefoundation.store6.devtools.storeTelemetryOf val logger = StoreTelemetryLogger() val monitor = StoreDevtoolsMonitor() val users = store { fetcher(userFetcher) telemetry(storeTelemetryOf(logger, monitor)) } ``` `storeTelemetryOf` invokes its sinks in registration order. Each sink must obey the telemetry contract: handlers are non-suspending, non-blocking, and never throw into Store correctness. The UI-free devtools artifact uses Store 6's full twelve-target convention: Android, JVM, `iosArm64`, `iosSimulatorArm64`, `iosX64`, `macosArm64`, `watchosArm64`, `tvosArm64`, JS, WasmJS, `linuxX64`, and `mingwX64`. ## The v0 log line `StoreTelemetryLogger` emits one line per event. Fields appear in this order: ```text