From f7a4f6048ff6dd60812df9e0a76c1f821ce0382a Mon Sep 17 00:00:00 2001 From: Roman Andrushchenko Date: Sun, 5 Jul 2026 12:24:03 +0300 Subject: [PATCH] feat: External query flows - add an alternative form of withQuery to the Store builders that accepts a lambda returning a query `Flow` instead of an imperative query owned by the store. --- README.md | 2 +- gradle-public.properties | 2 +- skills/container-store/README.md | 2 +- skills/container-store/SKILL.md | 76 +++-- skills/container-store/references/api.md | 200 +++++++++++- skills/container-store/references/patterns.md | 297 ++++++++++++++++++ store/README.md | 28 +- store/docs/keyed-store.md | 31 ++ store/docs/paged-store.md | 18 ++ store/docs/simple-store.md | 58 ++++ .../PagedExternalQueryStoreBuilders.kt | 90 ++++++ .../PagedKeyedExternalQueryStoreBuilders.kt | 81 +++++ .../store/builders/PagedKeyedStoreBuilders.kt | 66 ++++ .../store/builders/PagedStoreBuilders.kt | 66 ++++ .../SimpleExternalQueryStoreBuilders.kt | 166 ++++++++++ .../SimpleKeyedExternalQueryStoreBuilders.kt | 140 +++++++++ .../builders/SimpleKeyedStoreBuilders.kt | 129 ++++++++ .../store/builders/SimpleStoreBuilders.kt | 125 ++++++++ .../builders/keyed/PagedKeyedBuilderImpl.kt | 32 +- .../PagedKeyedExternalQueryBuilderImpl.kt | 61 ++++ ...KeyedExternalQuerySuspendingBuilderImpl.kt | 52 +++ .../keyed/PagedKeyedQueryBuilderImpl.kt | 4 +- .../PagedKeyedQuerySuspendingBuilderImpl.kt | 2 +- .../keyed/PagedKeyedSuspendingBuilderImpl.kt | 30 +- .../builders/keyed/SimpleKeyedBuilderImpl.kt | 32 +- .../SimpleKeyedExternalQueryBuilderImpl.kt | 71 +++++ ...leKeyedExternalQueryReactiveBuilderImpl.kt | 55 ++++ ...ternalQueryReactiveNoFetcherBuilderImpl.kt | 42 +++ ...KeyedExternalQuerySuspendingBuilderImpl.kt | 48 +++ .../keyed/SimpleKeyedQueryBuilderImpl.kt | 4 +- .../SimpleKeyedQueryReactiveBuilderImpl.kt | 2 +- ...eKeyedQueryReactiveNoFetcherBuilderImpl.kt | 2 +- .../SimpleKeyedQuerySuspendingBuilderImpl.kt | 2 +- .../keyed/SimpleKeyedReactiveBuilderImpl.kt | 29 +- ...SimpleKeyedReactiveNoFetcherBuilderImpl.kt | 29 +- .../keyed/SimpleKeyedSuspendingBuilderImpl.kt | 30 +- .../builders/paged/PagedBuilderImpl.kt | 32 +- .../paged/PagedExternalQueryBuilderImpl.kt | 67 ++++ ...PagedExternalQuerySuspendingBuilderImpl.kt | 62 ++++ .../builders/paged/PagedQueryBuilderImpl.kt | 4 +- .../paged/PagedQuerySuspendingBuilderImpl.kt | 2 +- .../paged/PagedSuspendingBuilderImpl.kt | 30 +- .../builders/simple/SimpleBuilderImpl.kt | 32 +- .../simple/SimpleExternalQueryBuilderImpl.kt | 80 +++++ .../SimpleExternalQueryReactiveBuilderImpl.kt | 69 ++++ ...ternalQueryReactiveNoFetcherBuilderImpl.kt | 55 ++++ ...impleExternalQuerySuspendingBuilderImpl.kt | 62 ++++ .../builders/simple/SimpleQueryBuilderImpl.kt | 4 +- .../simple/SimpleQueryReactiveBuilderImpl.kt | 2 +- ...SimpleQueryReactiveNoFetcherBuilderImpl.kt | 2 +- .../SimpleQuerySuspendingBuilderImpl.kt | 2 +- .../simple/SimpleReactiveBuilderImpl.kt | 29 +- .../SimpleReactiveNoFetcherBuilderImpl.kt | 29 +- .../simple/SimpleSuspendingBuilderImpl.kt | 30 +- .../internal/stores/KeyedQueryStoreImpl.kt | 11 +- .../stores/PagedKeyedQueryStoreImpl.kt | 11 +- .../store/internal/stores/PagedStoreImpl.kt | 6 +- .../store/internal/stores/SimpleStoreImpl.kt | 6 +- .../store/internal/stores/common/CoreStore.kt | 37 ++- .../keyed/KeyedExternalQueryStoreTest.kt | 100 ++++++ .../keyed/PagedKeyedExternalQueryStoreTest.kt | 103 ++++++ .../paged/PagedExternalQueryStoreTest.kt | 155 +++++++++ .../simple/SimpleExternalQueryStoreTest.kt | 274 ++++++++++++++++ 63 files changed, 3307 insertions(+), 93 deletions(-) create mode 100644 store/src/main/java/com/elveum/store/builders/PagedExternalQueryStoreBuilders.kt create mode 100644 store/src/main/java/com/elveum/store/builders/PagedKeyedExternalQueryStoreBuilders.kt create mode 100644 store/src/main/java/com/elveum/store/builders/SimpleExternalQueryStoreBuilders.kt create mode 100644 store/src/main/java/com/elveum/store/builders/SimpleKeyedExternalQueryStoreBuilders.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedExternalQueryBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedExternalQuerySuspendingBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryReactiveBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQuerySuspendingBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/paged/PagedExternalQueryBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/paged/PagedExternalQuerySuspendingBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryReactiveBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryReactiveNoFetcherBuilderImpl.kt create mode 100644 store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQuerySuspendingBuilderImpl.kt create mode 100644 store/src/test/java/com/elveum/store/keyed/KeyedExternalQueryStoreTest.kt create mode 100644 store/src/test/java/com/elveum/store/keyed/PagedKeyedExternalQueryStoreTest.kt create mode 100644 store/src/test/java/com/elveum/store/paged/PagedExternalQueryStoreTest.kt create mode 100644 store/src/test/java/com/elveum/store/simple/SimpleExternalQueryStoreTest.kt diff --git a/README.md b/README.md index 769bd63..a8a8178 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The full documentation is available [here](https://docs.uandcode.com/container). Add the following line to your `build.gradle` file: ``` -implementation "com.elveum:container:3.2.1" +implementation "com.elveum:container:3.3.0" ``` ## Core Concepts diff --git a/gradle-public.properties b/gradle-public.properties index f94b794..1db9ddc 100644 --- a/gradle-public.properties +++ b/gradle-public.properties @@ -6,7 +6,7 @@ android.nonTransitiveRClass=true # Maven Central publishing coordinates GROUP=com.elveum -VERSION_NAME=3.2.1 +VERSION_NAME=3.3.0 # POM metadata POM_DESCRIPTION=A library for simplifying state management and data loading in Android applications diff --git a/skills/container-store/README.md b/skills/container-store/README.md index 625a7a3..f3b1ad8 100644 --- a/skills/container-store/README.md +++ b/skills/container-store/README.md @@ -43,4 +43,4 @@ discovers skills (e.g. `~/.agents/skills/` for Codex), or paste | `references/api.md` | Every public import, `StoreResult` / `LoadRequest` semantics, typical operations | | `references/patterns.md` | Layer-by-layer patterns: data sources, repositories, ViewModels, Compose, DI, scoping | -The skill matches library version `3.2.1`. +The skill matches library version `3.3.0`. diff --git a/skills/container-store/SKILL.md b/skills/container-store/SKILL.md index c545d19..8486bb8 100644 --- a/skills/container-store/SKILL.md +++ b/skills/container-store/SKILL.md @@ -22,13 +22,13 @@ directly - no decompilation or dependency tree inspection needed. ## Dependency Setup -Maven coordinates: `com.elveum:store:3.2.1` (transitively brings +Maven coordinates: `com.elveum:store:3.3.0` (transitively brings `com.elveum:container`, whose types are part of the public API). ```toml # gradle/libs.versions.toml [versions] -store = "3.2.1" +store = "3.3.0" [libraries] store = { module = "com.elveum:store", version.ref = "store" } ``` @@ -59,6 +59,19 @@ List that grows while scrolling → paged; fully loaded list → simple store of `List`. `withQuery` is for runtime parameters that re-trigger fetching (search text, filters) - not a substitute for keyed stores. +`withQuery` has **two forms** (full signatures in +[references/api.md](references/api.md), "Queries (withQuery)"): + +- **Imperative** - `withQuery(initialQuery, debounceMillis = 0)`: the store owns + the query and exposes `queryFlow` + `submitQuery(...)` / `submitQueryAsync(...)`. + Returns a `*QueryStore` (e.g. `SimpleQueryStore`). +- **External flow** - `withQuery(debounceMillis = 0) { stateFlow }` or + `withQuery(initialQuery, debounceMillis = 0) { flow }`: the caller's + `Flow`/`StateFlow` owns the query; the store follows it and stays **plain** + (`SimpleStore`, no `submitQuery`). Keyed lambdas receive the key: + `withQuery { key -> flowFor(key) }`. Order-independent with local-storage / + `disableFetcher()` / `withKeys()` transitions. + ## Target Architecture ``` @@ -81,26 +94,29 @@ still appropriate. ## Quick Reference -| Operation | Call | -|-----------|------| -| Observe | `store.observe()` / `store.observe(key)` → `Flow>` | -| Read latest result synchronously | `store.get()` / `store.get(key)` → `StoreResult` | -| Read latest value/error synchronously | `store.getOrNull()` / `store.failureOrNull()` (key variants for keyed) | -| Local-only store (no remote fetcher) | `builder.disableFetcher().build(onObserve = { ... })` (simple & keyed) | -| Pagination: total item count | `PagedList(items, nextKey, totalCount = n)`; read `result.totalPagedItemsCount` (`-1` if unknown; shortcut for `result.metadata.totalPagedItemsCount`) | -| Await first completed result | `store.observe().firstGetOrThrow()` (suspend; value or throws) | -| Reload from the UI (try-again / pull-to-refresh) — PREFERRED | `result.invalidate()` on the rendered `StoreResult`; reloads the origin store with no ViewModel/repository plumbing | -| Pull-to-refresh (keep content visible) | Observe with `LoadRequest.Silent`, then `result.invalidate()`; progress shows via `result.isBackgroundLoading()` | -| Try-again (reload showing `Loading`) | `result.invalidate()` on the failed result (default request re-shows `Loading`) | -| Reload without a result at hand (old way, still valid) | `store.invalidateAsync()` / `store.invalidate()` (key variants for keyed) | -| Replace cached result outright | `store.updateWith(StoreResult.Loaded(new))` / `store.updateWith(key, ...)` | -| Read-modify-write loaded value | `store.updateIfSuccess { old -> new }` / `store.updateIfSuccess(key) { old -> new }` (no-op unless `Loaded`) | -| Optimistic update (auto-revert on failure) | `store.optimisticUpdate { old -> emit(new); realUpdate() }` | -| Submit query | `store.submitQueryAsync(query)` (no request argument) | -| Keys currently active (observed / in cache window) | `keyedStore.activeKeys` → `StateFlow>` | -| Pagination: report visible item | `store.onItemRendered(index)` | -| Pagination: next-page status | `result.nextPageState` (`Idle`/`Pending`/`Error(retry)`) | -| React while store is observed | `.whenActive { ... }` (chain after `build`) | +| Operation | Call | +|--------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| Observe | `store.observe()` / `store.observe(key)` → `Flow>` | +| Read latest result synchronously | `store.get()` / `store.get(key)` → `StoreResult` | +| Read latest value/error synchronously | `store.getOrNull()` / `store.failureOrNull()` (key variants for keyed) | +| Local-only store (no remote fetcher) | `builder.disableFetcher().build(onObserve = { ... })` (simple & keyed) | +| Pagination: total item count | `PagedList(items, nextKey, totalCount = n)`; read `result.totalPagedItemsCount` (`-1` if unknown; shortcut for `result.metadata.totalPagedItemsCount`) | +| Await first completed result | `store.observe().firstGetOrThrow()` (suspend; value or throws) | +| Reload from the UI (try-again / pull-to-refresh) — PREFERRED | `result.invalidate()` on the rendered `StoreResult`; reloads the origin store with no ViewModel/repository plumbing | +| Pull-to-refresh (keep content visible) | Observe with `LoadRequest.Silent`, then `result.invalidate()`; progress shows via `result.isBackgroundLoading()` | +| Try-again (reload showing `Loading`) | `result.invalidate()` on the failed result (default request re-shows `Loading`) | +| Reload without a result at hand (old way, still valid) | `store.invalidateAsync()` / `store.invalidate()` (key variants for keyed) | +| Replace cached result outright | `store.updateWith(StoreResult.Loaded(new))` / `store.updateWith(key, ...)` | +| Read-modify-write loaded value | `store.updateIfSuccess { old -> new }` / `store.updateIfSuccess(key) { old -> new }` (no-op unless `Loaded`) | +| Optimistic update (auto-revert on failure) | `store.optimisticUpdate { old -> emit(new); realUpdate() }` | +| Query: store owns it (imperative) | `builder.withQuery(initialQuery)` → `store.submitQueryAsync(query)` / `store.queryFlow` | +| Query: caller owns it (external state flow) | `builder.withQuery { stateFlow }` → plain store follows the flow (no `submitQuery`) | +| Query: caller owns it (external any flow) | `builder.withQuery(initialQuery) { anyFlow }` → plain store follows the flow (no `submitQuery`) | +| Keys currently active (observed / in cache window) | `keyedStore.activeKeys` → `StateFlow>` | +| Pagination: report visible item | `store.onItemRendered(index)` | +| Pagination: next-page status | `result.nextPageState` (`Idle`/`Pending`/`Error(retry)`) | +| React while store is observed (connect to other stores/events) | `.whenActive { ... }` chained after `build`; `this` is the store, block runs while observed & is cancelled on cache release (see patterns.md "Relations between stores") | +| Attach/read custom result flags (metadata) | `PagedList(items, nextKey, metadata = MyMeta(...))` / `emit(v, metadata = MyMeta(...))`; read `result.metadata.get()` (see api.md) | Cache behavior (all stores): lazy first fetch, one shared in-memory cache, released after the last observer unsubscribes plus @@ -135,6 +151,22 @@ cache, released after the last observer unsubscribes plus - Calling `optimisticUpdate`, `updateIfSuccess`, `submitQuery` or `submitQueryAsync` for a store/key with **no active observer** - these act on the in-memory cache, which only exists while the store (or key) is observed, so the call is silently a no-op. Observe first. +- Faking a fetcher to model a store with **no remote source** (an in-memory + value, session state, an event-bus stream): `build { awaitCancellation() }` + (stays `Loading` forever) or `build { Optional.empty() }` (one throwaway value). + Instead keep the value in a `MutableStateFlow` / `MutableSharedFlow` the app owns + and observe it with `disableFetcher().build(onObserve = { flow })`; to update, + mutate the flow directly (no `updateWith` / fake fetch). See patterns.md + "Externally-driven stores". +- Mishandling mutations (add/update/remove). A mutation always hits the backend; + what else depends on the local source. **With `addReactiveLocalStorage()`** (or a + `disableFetcher()` store observing a `Flow`): write to the backend and to the + local source (Room `@Insert`/`@Update`/`@Delete`) - the local `Flow` re-emits and + the store updates itself; do **not** also call `updateIfSuccess`/`updateWith`. + **Without a reactive source** (remote-only or `addSuspendingLocalStorage()`): + update every layer manually - backend, then suspending storage, then the store + cache via `updateIfSuccess { }` / `updateWith(...)` (or `optimisticUpdate { }`). + See patterns.md "Mutations (add / update / remove)". - Reaching for a `keyedStoreBuilder` - it was removed. Keyed stores are created by calling `.withKeys()` on a simple or paged builder (`simpleStoreBuilder().withKeys()`). diff --git a/skills/container-store/references/api.md b/skills/container-store/references/api.md index 569b5b8..993fefe 100644 --- a/skills/container-store/references/api.md +++ b/skills/container-store/references/api.md @@ -26,7 +26,7 @@ import com.elveum.store.stores.base.BaseStore import com.elveum.store.stores.base.BaseSimpleStore import com.elveum.store.stores.base.BasePagedStore import com.elveum.store.stores.base.WithQuery -import com.elveum.store.stores.base.WithStoreLifecycleOwner +import com.elveum.store.stores.base.WithStoreLifecycleOwner // base of every store; provides whenActive { } (chain after build) // Results and load requests import com.elveum.store.load.StoreResult // sealed: Loading / Loaded / Failed / Completed @@ -104,6 +104,9 @@ import com.elveum.store.exceptions.NoCachedDataException // emitted by offlineMo import com.elveum.container.subject.paging.PageState // Idle / Pending / Error(retry) import com.elveum.container.subject.paging.totalPagedItemsCount // ContainerMetadata.totalPagedItemsCount: Int (-1 if unknown) import com.elveum.container.subject.paging.TotalPagedItemsCountMetadata // attach a total count to a page +import com.elveum.container.ContainerMetadata // marker interface for attachable metadata; subclass it for custom flags +import com.elveum.container.get // metadata.get(): MyMetadata? - read one entry by type +import com.elveum.container.EmptyMetadata // the no-op metadata (PagedList / emit default) import com.elveum.container.SourceType // metadata: where the value came from import com.elveum.container.LocalSourceType import com.elveum.container.RemoteSourceType @@ -191,6 +194,54 @@ val sameUser: User? = userStore.getOptionalValueOrNull() // get(key) fo if (userStore.isOptionalEmpty()) showEmptyState() // isOptionalEmpty(key) for keyed ``` +### Container metadata (custom flags on a result) + +Every `StoreResult` carries a `ContainerMetadata` bag, readable as +`result.metadata`. The library ships typed entries (source type, background-load +state, total paged items count, ...) and you can attach **your own**. Reach for +this when the data source reports extra flags alongside the value — e.g. +`hasNextPage`, an ETag, a server `lastUpdated`, or a "stale" marker — instead of +widening the value type `T`. + +Define a metadata type (see `container/.../ContainerMetadata.kt` for the pattern — +any `data class`/`data object` implementing the marker interface): + +```kotlin +import com.elveum.container.ContainerMetadata +import import com.elveum.container.get + +data class PagingFlagsMetadata( + val hasNextPage: Boolean, +) : ContainerMetadata + +// Optional typed accessor, mirroring how the library exposes totalPagedItemsCount: +val ContainerMetadata.hasNextPage: Boolean + get() = get()?.hasNextPage ?: false +``` + +Attach it when producing the value: + +```kotlin +// Paged store — via the PagedList metadata argument (defaults to EmptyMetadata): +PagedList(items = page.items, nextKey = page.nextKey, metadata = PagingFlagsMetadata(page.hasNext)) +// Custom paged loader (buildCustom): emitPage(items, metadata = PagingFlagsMetadata(...)) +// Custom simple/keyed loader (buildCustom): emit(value, metadata = PagingFlagsMetadata(...)) +``` + +Read it back from any result: + +```kotlin +val flags = result.metadata.get() // PagingFlagsMetadata? (null if absent) +if (result.metadata.hasNextPage) { /* ... */ } // via the typed accessor +``` + +Combine several entries with `+` (a same-typed entry on the right replaces the +one on the left): `PagingFlagsMetadata(true) + EtagMetadata(tag)`. Metadata is +propagated from the loader/`PagedList` to the emitted `StoreResult`, so it is the +clean channel for out-of-band flags. The built-in `totalPagedItemsCount` shortcut +(and `defaultMetadata(...)`) are implemented exactly this way — `get()` plus a +default. + ### LoadRequest A `LoadRequest` is supplied in only two places: `observe(request? = null)` @@ -228,6 +279,113 @@ of always meaning `LoadRequest.Default`: DataStore/preferences: map the flag `Flow` into a `LoadRequest` (`if (offline) LoadRequest.builder().offlineMode().build() else LoadRequest.Default`). See patterns.md ("Reactive default request"). +- `whenActive(block: suspend Store.() -> Unit): Store` (chained after `build`) — + runs `block` **while the store is active** (from the first observer until the + cache is released) and cancels it when the store goes inactive. `this` inside + the block is the store itself, so the block can call `updateIfSuccess { }`, + `updateWith(...)`, `submitQueryAsync(...)`, etc. Use it to wire a store to + **external events / other stores** — e.g. subscribe to another store's update + events and patch this store's loaded value. Returns the same store, so the call + chains inline. See patterns.md ("Relations between stores"). Keyed variant + receives `KeyedStore` as `this` (use `updateIfSuccess(key) { }`). + +## Queries (withQuery) + +`withQuery` parameterizes a store by a runtime value (search text, filter, sort +order) that re-triggers fetching whenever it changes. There are **two families**, +chosen by whether the store or the caller owns the query value. Both accept an +optional `debounceMillis` (delay after a query change before reloading; default +`0`), and both make the `build(...)` fetch/storage lambdas receive the query `Q` +(`build { query -> ... }`, keyed `build { key, query -> ... }`). + +### 1. Imperative query — the store owns the query + +The store holds the current query and exposes an API to change it. Returns a +*query-aware* store (`SimpleQueryStore` / `KeyedQueryStore` / `PagedQueryStore` / +`PagedKeyedQueryStore`). + +Signature (present on every non-external builder: simple, suspending, reactive, +no-fetcher, keyed, paged): + +```kotlin +fun withQuery(initialQuery: Q, debounceMillis: Long = 0): +// simpleStoreBuilder() -> SimpleQueryBuilder -> SimpleQueryStore +// simpleStoreBuilder().withKeys() -> SimpleKeyedQueryBuilder -> KeyedQueryStore +// pagedStoreBuilder(...) -> PagedQueryBuilder -> PagedQueryStore +``` + +- `initialQuery` — query used for the first load (keyed: the seed for every key). + +Query API on the resulting store: + +```kotlin +store.queryFlow // StateFlow — current query +store.submitQueryAsync(newQuery) // fire-and-forget re-fetch +store.submitQuery(newQuery) // suspend variant; suspends until applied +// keyed store — each key holds its OWN query: +keyedStore.observeQueryFlow(key) // StateFlow for that key +keyedStore.submitQueryAsync(key, newQuery) +``` + +`submitQuery` / `submitQueryAsync` act on the in-memory cache and are a **no-op +when the store/key has no active observer** (observe first). + +### 2. External query flow — the caller owns the query + +The query comes from a `Flow`/`StateFlow` you already have (e.g. a ViewModel's +`searchText: StateFlow`). The store simply follows it. Returns a **plain** +store (`SimpleStore` / `KeyedStore` / `PagedStore` / `PagedKeyedStore`) with **no** +`queryFlow` / `submitQuery` — the external flow is the single source of truth. + +Two overloads on every builder that supports `withQuery`: + +```kotlin +// primary — explicit initial query; accepts any cold/hot Flow: +fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: () -> Flow, +): + +// convenience — StateFlow; initial query is taken from queryFlow().value: +fun withQuery( + debounceMillis: Long = 0, + queryFlow: () -> StateFlow, +): + +// keyed builders — the lambda receives the Key, so each key follows its own flow: +fun withQuery(initialQuery: Q, debounceMillis: Long = 0, queryFlow: (Key) -> Flow): ... +fun withQuery(debounceMillis: Long = 0, queryFlow: (Key) -> StateFlow): ... +``` + +Behaviour: + +- First load uses `initialQuery` (or `stateFlow.value`); each later emission + re-fetches (paged: resets to the first page). +- The flow is collected **only while the store/key is active** (has an observer, + or within the cache-timeout window); emissions while inactive do nothing. +- `build(...)` still returns the plain (non-query) store; contracts are the same + `...QueryContract` types as the imperative form. + +### Order independence + +`withQuery` may be chained **before or after** `addSuspendingLocalStorage()` / +`addReactiveLocalStorage()` / `disableFetcher()` / `withKeys()` — both orders +build the same store: + +```kotlin +simpleStoreBuilder().addSuspendingLocalStorage().withQuery { flow } // OK +simpleStoreBuilder().withQuery { flow }.addSuspendingLocalStorage() // same store +``` + +With keys, the ORDER picks the semantic: + +```kotlin +// ONE flow shared across all keys (every active key reloads on each emission): +simpleStoreBuilder().withQuery { sharedFlow }.withKeys() +// PER-KEY flow (each key follows its own stream): +simpleStoreBuilder().withKeys().withQuery { key -> flowFor(key) } +``` ## Typical Operations @@ -244,7 +402,10 @@ val value: T? = store.getOrNull() // = store.get().getOrNull( val error: Exception? = store.failureOrNull() // failureOrNull(key) for keyed // Local-only store (no remote fetcher): observe a local reactive Flow only. -// Supported by simple & keyed stores; combines with withQuery. +// Supported by simple & keyed stores; combines with withQuery. Also the right +// way to model an app-owned / event-bus value: observe a MutableStateFlow and +// mutate it to update (do NOT fake a fetcher like build { awaitCancellation() } +// or build { Optional.empty() } — see patterns.md "Externally-driven stores"). StoreFactory.simpleStoreBuilder() .disableFetcher() .build(onObserve = dataStore::observeSettings) // () -> Flow @@ -286,7 +447,11 @@ suspend fun refresh() = store.invalidate() // suspend until done val active: StateFlow> = keyedStore.activeKeys // Read-modify-write the loaded value AFTER the real data source changed -// (updateIfSuccess = the old update { } extension, renamed; runs only if Loaded): +// (updateIfSuccess = the old update { } extension, renamed; runs only if Loaded). +// NOTE: only needed WITHOUT a reactive local source. With addReactiveLocalStorage() +// (or a disableFetcher() store observing a Flow), write to the local source instead +// and let its Flow propagate - do NOT also call updateIfSuccess/updateWith. See +// patterns.md "Mutations (add / update / remove)". suspend fun renameProfile(newName: String) { dataSource.renameProfile(newName) store.updateIfSuccess { it.copy(name = newName) } @@ -317,13 +482,30 @@ store.updateIfSuccess(productId) { it.copy(isRead = true) } store.updateWith(StoreResult.Loaded(value)) store.updateWith(productId, StoreResult.Loaded(value)) // keyed store -// Query stores (submitQuery/submitQueryAsync take NO request): +// Query stores - see the "Queries (withQuery)" section above for the full +// signatures and the two families (imperative vs external flow). Quick calls: + +// Imperative form (store owns the query): store.queryFlow // StateFlow - current query store.submitQueryAsync(newQuery) // re-fetch -suspend fun search(q: Q) = store.submitQuery(q) // suspends - -// Keyed query store (withKeys() + withQuery()): each key holds its own query: -keyedQueryStore.observeQueryFlow(key) // StateFlow for that key +keyedQueryStore.observeQueryFlow(key) // StateFlow for that key (per-key query) keyedQueryStore.submitQueryAsync(key, newQuery) -suspend fun search(key: Key, q: Q) = keyedQueryStore.submitQuery(key, q) + +// External flow form (caller owns the query; result is a PLAIN store, no submitQuery): +val store = StoreFactory.simpleStoreBuilder>() + .withQuery(debounceMillis = 500) { searchQuery } // StateFlow: initial query = searchQuery.value + .build { query -> api.fetch(query) } // -> SimpleStore> + +// whenActive { } - run a coroutine while the store is observed (cancelled on cache +// release); chain it after build(). Use it to connect the store to external events +// or other stores. `this` is the store, so update helpers are callable directly: +val catsStore = StoreFactory.simpleStoreBuilder>() + .build(onFetch = catsDataSource::fetchCats) + .whenActive { // this: SimpleStore> + catEvents.observeCatEvents().collect { event -> + updateIfSuccess { cats -> // no-op unless currently Loaded + cats.map { if (it.id == event.cat.id) event.cat else it } + } + } + } ``` diff --git a/skills/container-store/references/patterns.md b/skills/container-store/references/patterns.md index 072dd18..e41577f 100644 --- a/skills/container-store/references/patterns.md +++ b/skills/container-store/references/patterns.md @@ -173,6 +173,159 @@ updates and queries. IMPORTANT: Usually there is NO need to set dispatcher via `setCoroutineContext(Dispatchers.IO)`, since most of modern IO operations (Room database, Retrofit) already operate on non-UI thread. +### Mutations (add / update / remove) + +A mutation (create/update/delete) must **always hit the backend**. What else you +must do depends on whether the store has a **reactive** local source attached +(`addReactiveLocalStorage()`, or a `disableFetcher()` store observing a local +`Flow`). + +**Case 1 — reactive local source attached.** The store's value is driven by the +local `Flow` (`onObserveStorage`). Write to the backend, then write to the **local +source** (a Room `@Insert`/`@Update`/`@Delete`); the local `Flow` re-emits and the +store updates itself. Do **not** also call `updateIfSuccess` / `updateWith` here - +the next local emission would overwrite it anyway. + +```kotlin +class NotesRepository( + storeFactory: StoreFactory, + private val notesApi: NotesApi, + private val notesDao: NotesDao, // exposes observeNotes(): Flow> +) { + private val store = storeFactory.simpleStoreBuilder>() + .addReactiveLocalStorage() + .build( + onFetch = { notesApi.getNotes().map { it.toDomain() } }, + onSaveToStorage = { notes -> notesDao.replaceAll(notes.map { it.toEntity() }) }, + onObserveStorage = { notesDao.observeNotes().map { row -> row.map { it.toDomain() } } }, + ) + + fun observeNotes(): Flow>> = store.observe() + + suspend fun addNote(draft: NoteDraft) { + val created = notesApi.createNote(draft) // 1) backend + notesDao.insert(created.toEntity()) // 2) local source -> Flow re-emits -> store updates + } // (no store call needed) + + suspend fun deleteNote(id: Long) { + notesApi.deleteNote(id) // backend + notesDao.deleteById(id) // local source -> propagates to the store + } +} +``` + +**Case 2 — no reactive local source** (remote-only, or `addSuspendingLocalStorage()`). +Nothing propagates automatically, so update every layer **manually**: backend, then +the suspending storage (if any), then the **store cache** via `updateIfSuccess { }` +(read-modify-write; no-op unless currently `Loaded`) or `updateWith(...)`. The +storage write keeps the next cold load consistent; the store-cache update reflects +the change to current observers immediately. + +```kotlin +class NotesRepository( + storeFactory: StoreFactory, + private val notesApi: NotesApi, + private val notesDao: NotesDao, // suspend save/load only (no Flow) +) { + private val store = storeFactory.simpleStoreBuilder>() + .addSuspendingLocalStorage() + .build( + onFetch = { notesApi.getNotes().map { it.toDomain() } }, + onSaveToStorage = { notes -> notesDao.replaceAll(notes.map { it.toEntity() }) }, + onLoadFromStorage = { notesDao.getAll()?.map { it.toDomain() } }, + ) + + fun observeNotes(): Flow>> = store.observe() + + suspend fun addNote(draft: NoteDraft) { + val created = notesApi.createNote(draft) // 1) backend + notesDao.insert(created.toEntity()) // 2) suspending storage (next cold load) + store.updateIfSuccess { it + created.toDomain() } // 3) store cache (current observers) + } + + suspend fun deleteNote(id: Long) { + notesApi.deleteNote(id) // backend + notesDao.deleteById(id) // storage + store.updateIfSuccess { list -> list.filterNot { it.id == id } } // store cache + } +} +``` + +For a **remote-only** store (no local storage at all) it is the same as Case 2 +minus the storage write: hit the backend, then `updateIfSuccess { }` / +`updateWith(...)` the store cache. + +Notes: + +- `updateIfSuccess` / `updateWith` act on the **in-memory cache**, which exists + only while the store is observed. If the store may currently be unobserved, the + storage write (step 2) is what makes the change survive; on the next observe the + store reloads from storage. (With a reactive source, Case 1, the local write both + persists and propagates - one step.) +- For a snappy UI that also rolls back on failure, wrap Case 2 in + `optimisticUpdate { old -> emit(newValue); backendCall() }` - emitted values + auto-revert if the block throws. (With a reactive source, prefer an optimistic + write to the local source if it supports it, or just accept the Flow round-trip.) +- Keyed store: use the key-scoped variants (`updateIfSuccess(key) { }`, + `updateWith(key, ...)`, `optimisticUpdate(key) { }`). +- Paged store has **no** reactive local storage, so it is always Case 2: update the + backend, then patch the paged cache with `updateIfSuccess { }` / + `optimisticUpdate { }`, or call `invalidate()` to reload from the first page. + +### Externally-driven stores (event bus / in-memory state) - no fabricated fetcher + +Some stores have **no remote source**: the value is produced inside the app and +changes over time (an in-memory selection, session/auth state, an event-bus-like +stream). Do **not** fake a fetcher to model this - e.g. +`build { awaitCancellation() }` (never returns, so the store stays `Loading` +forever) or `build { Optional.empty() }` (one throwaway value, then updates only +via `updateWith`). Those hacks fight the library and break `Loading`/reload +semantics. + +Instead, keep the value in a `MutableStateFlow` (or `MutableSharedFlow`) that the +app owns, and let the store **observe** it via `disableFetcher()`. To change the +value, mutate the flow directly - the store re-emits automatically: + +```kotlin +import com.elveum.store.StoreFactory +import com.elveum.store.load.StoreResult +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class SelectedFilterRepository( + storeFactory: StoreFactory, +) { + // app-owned source of truth (also can be a flow from Android Data Store, Room, + // shared preferences, etc.): + private val selectedFilter = MutableStateFlow(Filter.Default) + + private val store = storeFactory.simpleStoreBuilder() + .disableFetcher() // no remote fetch + .build(onObserve = { selectedFilter }) // () -> Flow + + fun observeFilter(): Flow> = store.observe() + + // "update the value" = mutate the external flow directly (no updateWith / fake fetch): + fun setFilter(filter: Filter) { selectedFilter.value = filter } +} +``` + +Notes: + +- Use a `MutableStateFlow` when there is always a current value (the store becomes + `Loaded` immediately). Use a `MutableSharedFlow(replay = 1)` for a pure event + stream with no initial value (the store stays `Loading` until the first emit). +- Keyed variant: `simpleStoreBuilder().withKeys().disableFetcher().build { key -> flowFor(key) }`, + holding one `MutableStateFlow` per key. +- This is **not** the same as `updateWith` / `updateIfSuccess`: those act on the + in-memory cache **only while observed** and are for reflecting a change that + already happened in a real data source. For an app-owned value that IS the + source, hold it in a flow and observe it - the flow survives cache release, + a value pushed with `updateWith` does not. +- If the value must ALSO be fetched remotely once and then react to local pushes, + that is a real data source: use `addReactiveLocalStorage()` (remote fetch + + observe a local `Flow`) instead of `disableFetcher()`. + ### Reactive default request (runtime-controlled loading policy) `setLoadRequest` has two overloads: a fixed `setLoadRequest(LoadRequest)` and a @@ -213,6 +366,150 @@ class ArticlesRepository( Available on every builder (simple, keyed, paged, and their query variants). +### Query stores (search / filter / sort) + +A *query* is any runtime parameter that re-triggers fetching (search text, +filter, sort order). `withQuery` comes in two forms - pick by **who owns the +query value**. Full signatures are in [api.md](api.md) ("Queries (withQuery)"). + +**A. Store owns the query** - `withQuery(initialQuery, debounceMillis)`. The +store keeps the current query and exposes `queryFlow` + `submitQueryAsync(...)`; +the repository forwards those: + +```kotlin +import com.elveum.store.StoreFactory +import com.elveum.store.load.StoreResult +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +class ImageSearchRepository( + storeFactory: StoreFactory, + private val imagesDataSource: ImagesDataSource, +) { + private val store = storeFactory.simpleStoreBuilder>() + .withQuery(initialQuery = "", debounceMillis = 300) // -> SimpleQueryStore> + .build { query -> imagesDataSource.search(query) } // fetch lambda receives the query + + fun observeImages(): Flow>> = store.observe() + fun currentQuery(): StateFlow = store.queryFlow // e.g. to prefill the search field + fun search(query: String) = store.submitQueryAsync(query) // fire-and-forget re-fetch +} +``` + +**B. Caller owns the query** - `withQuery { flow }`. The query already lives in an +external `StateFlow`. Pass that flow to the builder: the store follows it and stays +a **plain** `SimpleStore` (no `submitQuery`), so the query is never mirrored in two places: + +```kotlin +import com.elveum.store.StoreFactory +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class ImageSearchRepository( + private val storeFactory: StoreFactory, + private val imagesDataSource: ImagesDataSource, +) { + + private val _externalQueryFlow = MutableStateFlow("") + val externalQueryFlow: StateFlow = _externalQueryFlow + + private val store = storeFactory.simpleStoreBuilder>() + // if external flow is StateFlow -> no need to manually set an initial query - it is derived from StateFlow.value + .withQuery(debounceMillis = 300) { externalQueryFlow } + .build { q -> imagesDataSource.search(q) } + + fun submitQuery(query: String) = _externalQueryFlow.update { query } +} +``` + +If a query is supplied by ViewModel, it is better to scope the store to the ViewModel lifecycle. +But if it is not possible (e.g. the repository must be a singleton), then: + +- Create a function providing the store instance +- Optional: use `whenActive` block to react on external events and update data in the store (if needed) +- Ideally, return not a store, but observable `Flow` + +```kotlin +class ImageSearchRepository( + private val storeFactory: StoreFactory, + private val imagesDataSource: ImagesDataSource, +) { + + // any ViewModel can call this function and pass queryFlow + fun observeImages(queryFlow: StateFlow): Flow>> = + storeFactory.simpleStoreBuilder>() + .withQuery(debounceMillis = 300) { queryFlow } + .build { query -> imagesDataSource.search(query) } + .whenActive { + // OPTIONAL: + // subscribe to other dependencies that affects the store (if needed) + // 'this' within whenActive points to the Store itself + } + .observe() +} +``` + +Notes: + +- The external-flow overload has two forms: `withQuery(debounceMillis) { stateFlow }` + (initial query = `stateFlow.value`) and `withQuery(initialQuery, debounceMillis) { flow }` + for a plain `Flow` that may not emit synchronously. +- The flow is collected only while the store is observed; the first load uses the + initial query, each later emission re-fetches (paged: resets to the first page). +- `withQuery` combines with local storage and `disableFetcher()` in **any order**, + and the `build(...)` lambdas receive the query (`build { q -> ... }`). +- Keyed **per-key** query: `simpleStoreBuilder().withKeys().withQuery { key -> flowFor(key) }`. + Reversed, `withQuery { flow }.withKeys()` **shares one flow across all keys**. + +### Custom result metadata (flags beyond the value) + +When a data source returns extra flags alongside the data (`hasNextPage`, an +ETag, `lastUpdatedAt`, a "stale" marker), attach them as **custom +`ContainerMetadata`** rather than widening the value type. The store propagates +metadata to the emitted `StoreResult`, and the ViewModel/composable reads it back +with `result.metadata.get()`. See [api.md](api.md) ("Container metadata") for +the full API and `ContainerMetadata.kt` for how to define a metadata type. + +```kotlin +import com.elveum.container.ContainerMetadata +import com.elveum.container.get +import com.elveum.store.StoreFactory +import com.elveum.store.load.StoreResult +import com.elveum.store.stores.paged.PagedList +import kotlinx.coroutines.flow.Flow + +data class HasNextPageMetadata(val hasNextPage: Boolean) : ContainerMetadata + +class FeedRepository( + storeFactory: StoreFactory, + private val feedDataSource: FeedDataSource, +) { + private val store = storeFactory.pagedStoreBuilder(initialKey = 0, itemId = Post::id) + .build( + onFetch = { pageKey -> + val page = feedDataSource.fetchPage(pageKey) + PagedList( + items = page.posts, + nextKey = page.nextKey, + metadata = HasNextPageMetadata(page.hasNext), // attach the flag + ) + } + ) + + fun getFeed(): Flow>> = store.observe() +} + +// In the ViewModel / composable, read it back off the rendered result: +val hasNext = (result as? StoreResult.Loaded)?.metadata?.get()?.hasNextPage ?: false +``` + +Prefer this over the `PagedList(items, nextKey, totalCount = n)` convenience +constructor when the API gives you flags (like `hasNextPage`) rather than a total +count - that convenience constructor merely attaches the built-in +`TotalPagedItemsCountMetadata`; custom metadata works the same way. Combine +multiple entries with `+` (`HasNextPageMetadata(true) + EtagMetadata(tag)`). + ### Relations between stores (entity dependencies) When data managed by one store depends on another store (e.g. editing an diff --git a/store/README.md b/store/README.md index 1674693..ff7ba61 100644 --- a/store/README.md +++ b/store/README.md @@ -38,7 +38,7 @@ the hood. Add the following line to your `build.gradle` file: ``` -implementation "com.elveum:store:3.2.1" +implementation "com.elveum:store:3.3.0" ``` The `store` artifact depends on `com.elveum:container`, so the Container @@ -282,6 +282,32 @@ fun setQuery(query: String) = store.submitQueryAsync(query) val currentQuery: StateFlow get() = store.queryFlow ``` +### External query flow + +When the query already lives elsewhere as a reactive stream (a search +`StateFlow`, a combination of filters), pass it to `withQuery` as a lambda +returning a `Flow` instead of calling `submitQuery` yourself. The +store follows that flow and is a plain store (`SimpleStore` / `KeyedStore` / +`PagedStore`) with **no** `submitQuery`/`queryFlow` surface: + +```kotlin +private val searchQuery = MutableStateFlow("") +private val store = StoreFactory.simpleStoreBuilder>() + .withQuery(debounceMillis = 500) { searchQuery } // searchQuery: StateFlow + .build { query -> remoteSource.fetchImages(query) } // -> SimpleStore> + +fun getImages(): Flow>> = store.observe() +fun setSearchQuery(query: String) = searchQuery.update { query } +``` + +A `StateFlow` needs no initial query (it is derived from `.value`). For a plain +`Flow`, supply an `initialQuery` for the immediate first load: +`withQuery(initialQuery = "") { queryFlow }`. Keyed stores receive the key, so +each key can follow its own query flow: +`withQuery { key -> queryFlowFor(key) }`. For paged stores a new query resets +pagination and reloads from the first page. This overload is available on every +builder that supports `withQuery`. + ## Updating Cached Data All stores support optimistic updates: emit the expected new value to the diff --git a/store/docs/keyed-store.md b/store/docs/keyed-store.md index 008d835..bda1325 100644 --- a/store/docs/keyed-store.md +++ b/store/docs/keyed-store.md @@ -263,6 +263,37 @@ Submitting a new query for a key reloads only that key. Like `SimpleKeyedQueryReactiveContract`, `SimpleKeyedQueryReactiveNoFetcherContract`. +### External query flow + +Instead of calling `submitQuery(key, …)`, you can let each key follow an +external query `Flow`. The `withQuery` lambda receives the key, so every key can +have its own query stream (for example, a per-key filter `StateFlow`). The +result is a plain `KeyedStore` with no query API: + +```kotlin +private val store = StoreFactory.simpleStoreBuilder() + .withKeys() + .withQuery(initialQuery = ReviewQuery(sort = Sort.Newest)) { productId -> + sortPreferences.reviewQueryFor(productId) // Flow + } + .build { productId, query -> api.fetchReviews(productId, query) } // -> KeyedStore +``` + +For a `StateFlow` source, drop `initialQuery` - each key's initial query is +derived from that key's `StateFlow.value`: +`withQuery { key -> queryStateFlowFor(key) }`. Only the key whose flow emits is +reloaded; other keys keep their current query and cached value. This overload is +available on the remote-only, suspending, reactive and `disableFetcher()` keyed +builders. See [Simple Store](simple-store.md#external-query-flow) for the +overload semantics. + +The keyed lambda above (`withKeys()` first) gives each key its **own** query +flow. If instead you call the non-keyed `withQuery { flow }` and then +`withKeys()`, that single flow is **shared by every key** - a global query +stream driving all keys at once. Both orderings are supported, and `withQuery` +can be applied before or after `addSuspendingLocalStorage()` / +`addReactiveLocalStorage()` / `disableFetcher()`. + ## Paged Keyed Stores Calling `.withKeys()` on a [paged builder](paged-store.md) produces a diff --git a/store/docs/paged-store.md b/store/docs/paged-store.md index abf8ce3..384cbb3 100644 --- a/store/docs/paged-store.md +++ b/store/docs/paged-store.md @@ -236,6 +236,24 @@ parameter for search-as-you-type scenarios. `submitQuery` / `submitQueryAsync` take no `LoadRequest`; submitting a query keeps the previous results visible while the new query loads. +### External query flow + +Instead of driving the query with `submitQuery`, pass a reactive query stream to +`withQuery` as a lambda returning a `Flow`. The store follows that flow - a new +query resets pagination and reloads from the first page - and the result is a +plain `PagedStore` with no query API: + +```kotlin +private val store = StoreFactory + .pagedStoreBuilder(initialKey = 0, itemId = Photo::id) + .withQuery(debounceMillis = 300) { categoryFilter } // StateFlow> + .build { query, pageKey -> photoDataSource.fetchPage(query, pageKey) } // -> PagedStore +``` + +For a `StateFlow` the initial query is derived from `.value`; for a plain `Flow` +supply an `initialQuery` for the immediate first load +(`withQuery(initialQuery) { flow }`). + ## Local Storage Paged stores support the same local storage modes as other stores. The diff --git a/store/docs/simple-store.md b/store/docs/simple-store.md index 1f2f984..60b85ef 100644 --- a/store/docs/simple-store.md +++ b/store/docs/simple-store.md @@ -316,6 +316,64 @@ Key points: takes a `LoadRequest` - the loading behaviour follows the request each observer subscribed with via `observe(...)` (or the builder default). +### External query flow + +Instead of driving the query imperatively with `submitQuery`, you can pass an +existing reactive query stream to `withQuery` as a lambda returning a `Flow`. +The store then follows that flow, and the result is a plain `SimpleStore` +with no `submitQuery`/`queryFlow` of its own - the flow is the single source of +truth for the query: + +```kotlin +class GalleryRepository( + private val remoteSource: RemoteGalleryDataSource, + private val localSource: LocalGalleryDataSource, +) { + + private val searchQuery = MutableStateFlow("") + + private val store = StoreFactory.simpleStoreBuilder>() + .addSuspendingLocalStorage() + .withQuery(debounceMillis = 500) { searchQuery } + .build( + onFetch = remoteSource::fetchImages, // suspend (Q) -> T + onSaveToStorage = localSource::saveImages, // suspend (Q, T) -> Unit + onLoadFromStorage = localSource::loadImages, // suspend (Q) -> T? + ) + + fun getImages(): Flow>> = store.observe() +} +``` + +There are two overloads: + +- `withQuery(debounceMillis) { stateFlow }` - for a `StateFlow`, the initial + query is taken from `StateFlow.value`, so the first load happens immediately. +- `withQuery(initialQuery, debounceMillis) { flow }` - for a plain `Flow` that + may not emit synchronously, `initialQuery` seeds the immediate first load and + the flow drives every reload after. + +Both are available on the remote-only, suspending, reactive and +`disableFetcher()` (local-only) simple builders. An emission equal to the +current query does not trigger a redundant reload, and the flow is collected +only while the store is active. + +`withQuery` is **order-independent** - it can be applied before or after +`addSuspendingLocalStorage()`, `addReactiveLocalStorage()`, `disableFetcher()` +and `withKeys()`, so all of these compile and behave identically: + +```kotlin +StoreFactory.simpleStoreBuilder().withQuery { flow }.addSuspendingLocalStorage().build(/* ... */) +StoreFactory.simpleStoreBuilder().addSuspendingLocalStorage().withQuery { flow }.build(/* ... */) +``` + +Note the two ways to combine an external query with `withKeys` differ in +semantics (see [Keyed Store](keyed-store.md#external-query-flow)): + +- `withKeys().withQuery { key -> flowFor(key) }` - **per-key** query flows. +- `withQuery { flow }.withKeys()` - the single flow is **shared by every key** + (one global query stream driving all keys). + ## Updating Cached Data `optimisticUpdate` lets you show changes instantly while a long-running diff --git a/store/src/main/java/com/elveum/store/builders/PagedExternalQueryStoreBuilders.kt b/store/src/main/java/com/elveum/store/builders/PagedExternalQueryStoreBuilders.kt new file mode 100644 index 0000000..365c381 --- /dev/null +++ b/store/src/main/java/com/elveum/store/builders/PagedExternalQueryStoreBuilders.kt @@ -0,0 +1,90 @@ +package com.elveum.store.builders + +import com.elveum.container.subject.paging.PageEmitter +import com.elveum.store.contracts.PagedQueryContract +import com.elveum.store.contracts.PagedQuerySuspendingContract +import com.elveum.store.stores.paged.PagedList +import com.elveum.store.stores.paged.PagedStore + +/** + * Builder for a remote-only [PagedStore] whose query is driven by an external + * [kotlinx.coroutines.flow.Flow]. + * + * Created via `PagedBuilder.withQuery { flow }`. The store follows the supplied query flow - + * a new query resets pagination and reloads from the first page - and exposes no query API of + * its own. + * + * @param Q the type representing the query. + * @param PageKey the type used as a pagination cursor or page identifier. + * @param T the type of individual items in the paged list. + */ +public interface PagedExternalQueryBuilder : + BasePagedBuilder> { + + /** + * Transitions the builder into a keyed variant. The configured external query flow is shared + * by every key (each key follows the same query stream). For per-key query flows, call + * `withKeys()` before `withQuery { key -> ... }` instead. + * + * @param Key the type of the keys managed by the resulting store. + */ + public fun withKeys(): PagedKeyedExternalQueryBuilder + + /** + * Transitions the builder to a variant that supports a suspending (one-shot) local storage + * layer, preserving the configured external query flow. + */ + public fun addSuspendingLocalStorage(): PagedExternalQuerySuspendingBuilder + + /** + * Builds a [PagedStore] using the provided [PagedQueryContract] implementation. + */ + public fun build(contract: PagedQueryContract): PagedStore + + /** + * Builds a [PagedStore] using a single lambda that fetches a page for the current query and + * a page key. + */ + public fun build(onFetch: suspend (Q, PageKey) -> PagedList): PagedStore + + /** + * Builds a [PagedStore] with a custom loader that emits pages manually through a + * [PageEmitter], receiving the current query and page key. + */ + public fun buildCustom(loader: suspend PageEmitter.(Q, PageKey) -> Unit): PagedStore +} + +/** + * Builder for a [PagedStore] backed by suspending local storage whose query is driven by an + * external [kotlinx.coroutines.flow.Flow]. Created via `PagedSuspendingBuilder.withQuery { flow }`. + * + * @param Q the type representing the query. + * @param PageKey the type used as a pagination cursor or page identifier. + * @param T the type of individual items in the paged list. + */ +public interface PagedExternalQuerySuspendingBuilder : + BasePagedBuilder> { + + /** + * Transitions the builder into a keyed variant with suspending local storage. The configured + * external query flow is shared by every key. + * + * @param Key the type of the keys managed by the resulting store. + */ + public fun withKeys(): PagedKeyedExternalQuerySuspendingBuilder + + /** + * Builds a [PagedStore] using the provided [PagedQuerySuspendingContract] implementation. + */ + public fun build(contract: PagedQuerySuspendingContract): PagedStore + + /** + * Builds a [PagedStore] using individual lambdas for remote page fetch and suspending local + * storage, each receiving the current query and page key. + */ + public fun build( + onFetch: suspend (Q, PageKey) -> PagedList, + onSaveToStorage: suspend (Q, PageKey, PagedList) -> Unit, + onLoadFromStorage: suspend (Q, PageKey) -> PagedList?, + ): PagedStore +} diff --git a/store/src/main/java/com/elveum/store/builders/PagedKeyedExternalQueryStoreBuilders.kt b/store/src/main/java/com/elveum/store/builders/PagedKeyedExternalQueryStoreBuilders.kt new file mode 100644 index 0000000..f860fb2 --- /dev/null +++ b/store/src/main/java/com/elveum/store/builders/PagedKeyedExternalQueryStoreBuilders.kt @@ -0,0 +1,81 @@ +package com.elveum.store.builders + +import com.elveum.container.subject.paging.PageEmitter +import com.elveum.store.contracts.PagedKeyedQueryContract +import com.elveum.store.contracts.PagedKeyedQuerySuspendingContract +import com.elveum.store.stores.keyed.PagedKeyedStore +import com.elveum.store.stores.paged.PagedList + +/** + * Builder for a remote-only keyed [PagedKeyedStore] whose per-key query is driven by an external + * [kotlinx.coroutines.flow.Flow]. + * + * Created via `PagedKeyedBuilder.withQuery { key -> flow }`. Each key follows its own query flow - + * a new query for a key resets that key's pagination and reloads from the first page - and the + * resulting store exposes no query API of its own. + * + * @param Key the type of the keys managed by the store. + * @param Q the type representing the query. + * @param PageKey the type of the page key used to request the next page. + * @param T the type of the items contained in each key's paged list. + */ +public interface PagedKeyedExternalQueryBuilder : + BasePagedBuilder> { + + /** + * Transitions the builder to a variant that supports a suspending (one-shot) local storage + * layer, preserving the configured per-key external query flow. + */ + public fun addSuspendingLocalStorage(): PagedKeyedExternalQuerySuspendingBuilder + + /** + * Builds a [PagedKeyedStore] using the provided [PagedKeyedQueryContract] implementation. + */ + public fun build(contract: PagedKeyedQueryContract): PagedKeyedStore + + /** + * Builds a [PagedKeyedStore] using a single lambda that fetches one page of a key's list for + * a given query. + */ + public fun build( + onFetch: suspend (Key, Q, PageKey) -> PagedList, + ): PagedKeyedStore + + /** + * Builds a [PagedKeyedStore] with a custom loader that emits pages manually through a + * [PageEmitter], receiving the key, current query and page key. + */ + public fun buildCustom( + loader: suspend PageEmitter.(Key, Q, PageKey) -> Unit, + ): PagedKeyedStore +} + +/** + * Builder for a keyed [PagedKeyedStore] backed by suspending local storage whose per-key query is + * driven by an external [kotlinx.coroutines.flow.Flow]. Created via + * `PagedKeyedSuspendingBuilder.withQuery { key -> flow }`. + * + * @param Key the type of the keys managed by the store. + * @param Q the type representing the query. + * @param PageKey the type of the page key used to request the next page. + * @param T the type of the items contained in each key's paged list. + */ +public interface PagedKeyedExternalQuerySuspendingBuilder : + BasePagedBuilder> { + + /** + * Builds a [PagedKeyedStore] using the provided [PagedKeyedQuerySuspendingContract] + * implementation. + */ + public fun build(contract: PagedKeyedQuerySuspendingContract): PagedKeyedStore + + /** + * Builds a [PagedKeyedStore] using individual lambdas for page fetch and suspending local + * storage, each receiving the key, current query and page key. + */ + public fun build( + onFetch: suspend (Key, Q, PageKey) -> PagedList, + onSaveToStorage: suspend (Key, Q, PageKey, PagedList) -> Unit, + onLoadFromStorage: suspend (Key, Q, PageKey) -> PagedList?, + ): PagedKeyedStore +} diff --git a/store/src/main/java/com/elveum/store/builders/PagedKeyedStoreBuilders.kt b/store/src/main/java/com/elveum/store/builders/PagedKeyedStoreBuilders.kt index 84df5be..9f25ea1 100644 --- a/store/src/main/java/com/elveum/store/builders/PagedKeyedStoreBuilders.kt +++ b/store/src/main/java/com/elveum/store/builders/PagedKeyedStoreBuilders.kt @@ -8,6 +8,8 @@ import com.elveum.store.contracts.PagedKeyedSuspendingContract import com.elveum.store.stores.keyed.PagedKeyedQueryStore import com.elveum.store.stores.keyed.PagedKeyedStore import com.elveum.store.stores.paged.PagedList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** * Builder for creating a keyed [PagedKeyedStore] without local storage (remote-only), @@ -39,6 +41,38 @@ public interface PagedKeyedBuilder : debounceMillis: Long = 0, ): PagedKeyedQueryBuilder + /** + * Transitions the builder to a variant whose per-key query is driven by an external [Flow]. + * Each key performs an immediate first load using [initialQuery]; whenever the flow returned by + * [queryFlow] for a key emits a new query, that key's pagination is reset and reloaded from the + * first page. The resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query each key uses for its immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow] for a given key. + * @return a [PagedKeyedExternalQueryBuilder] driven by the given per-key query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: (Key) -> Flow, + ): PagedKeyedExternalQueryBuilder + + /** + * Convenience overload for a [StateFlow] query source: each key's initial query is derived from + * that key's [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow] for a given key. + * @return a [PagedKeyedExternalQueryBuilder] driven by the given per-key query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: (Key) -> StateFlow, + ): PagedKeyedExternalQueryBuilder + /** * Transitions the builder to a variant that supports a suspending (one-shot) * local storage layer. @@ -157,6 +191,38 @@ public interface PagedKeyedSuspendingBuilder initialQuery: Q, debounceMillis: Long = 0, ): PagedKeyedQuerySuspendingBuilder + /** + * Transitions the builder to a variant whose per-key query is driven by an external [Flow], + * backed by suspending local storage. Each key performs an immediate first load using + * [initialQuery]; whenever [queryFlow] for a key emits a new query, that key's pagination is + * reset and reloaded from the first page. The resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query each key uses for its immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow] for a given key. + * @return a [PagedKeyedExternalQuerySuspendingBuilder] driven by the given per-key query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: (Key) -> Flow, + ): PagedKeyedExternalQuerySuspendingBuilder + + /** + * Convenience overload for a [StateFlow] query source: each key's initial query is derived from + * that key's [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow] for a given key. + * @return a [PagedKeyedExternalQuerySuspendingBuilder] driven by the given per-key query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: (Key) -> StateFlow, + ): PagedKeyedExternalQuerySuspendingBuilder + /** * Builds a [PagedKeyedStore] using the provided [PagedKeyedSuspendingContract] * implementation. diff --git a/store/src/main/java/com/elveum/store/builders/PagedStoreBuilders.kt b/store/src/main/java/com/elveum/store/builders/PagedStoreBuilders.kt index 0f708d2..14940a6 100644 --- a/store/src/main/java/com/elveum/store/builders/PagedStoreBuilders.kt +++ b/store/src/main/java/com/elveum/store/builders/PagedStoreBuilders.kt @@ -9,6 +9,8 @@ import com.elveum.store.contracts.PagedSuspendingContract import com.elveum.store.stores.paged.PagedList import com.elveum.store.stores.paged.PagedQueryStore import com.elveum.store.stores.paged.PagedStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** * Base interface for paged store builders, extending [BaseBuilder] with pagination-specific @@ -62,6 +64,38 @@ public interface PagedBuilder : BasePagedBuilder withQuery(initialQuery: Q, debounceMillis: Long = 0): PagedQueryBuilder + /** + * Transitions the builder to a variant whose query is driven by an external [Flow]. + * The store performs an immediate first load using [initialQuery]; whenever [queryFlow] + * emits a new query the pagination is reset and reloaded from the first page. The resulting + * store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query used for the immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow]. + * @return a [PagedExternalQueryBuilder] driven by the given query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: () -> Flow, + ): PagedExternalQueryBuilder + + /** + * Convenience overload for a [StateFlow] query source: the initial query is derived from + * [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow]. + * @return a [PagedExternalQueryBuilder] driven by the given query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: () -> StateFlow, + ): PagedExternalQueryBuilder + /** * Transitions the builder to a variant that supports a suspending (one-shot) * local storage layer. @@ -188,6 +222,38 @@ public interface PagedSuspendingBuilder : initialQuery: Q, debounceMillis: Long = 0, ): PagedQuerySuspendingBuilder + /** + * Transitions the builder to a variant whose query is driven by an external [Flow], backed by + * suspending local storage. The store performs an immediate first load using [initialQuery]; + * whenever [queryFlow] emits a new query the pagination is reset and reloaded from the first + * page. The resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query used for the immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow]. + * @return a [PagedExternalQuerySuspendingBuilder] driven by the given query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: () -> Flow, + ): PagedExternalQuerySuspendingBuilder + + /** + * Convenience overload for a [StateFlow] query source: the initial query is derived from + * [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow]. + * @return a [PagedExternalQuerySuspendingBuilder] driven by the given query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: () -> StateFlow, + ): PagedExternalQuerySuspendingBuilder + /** * Builds a [PagedStore] using the provided [PagedSuspendingContract] implementation. * diff --git a/store/src/main/java/com/elveum/store/builders/SimpleExternalQueryStoreBuilders.kt b/store/src/main/java/com/elveum/store/builders/SimpleExternalQueryStoreBuilders.kt new file mode 100644 index 0000000..3b07761 --- /dev/null +++ b/store/src/main/java/com/elveum/store/builders/SimpleExternalQueryStoreBuilders.kt @@ -0,0 +1,166 @@ +package com.elveum.store.builders + +import com.elveum.container.Emitter +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleQueryContract +import com.elveum.store.contracts.SimpleQueryReactiveContract +import com.elveum.store.contracts.SimpleQueryReactiveNoFetcherContract +import com.elveum.store.contracts.SimpleQuerySuspendingContract +import com.elveum.store.stores.simple.SimpleStore +import kotlinx.coroutines.flow.Flow + +/** + * Builder for a remote-only [SimpleStore] whose query is driven by an external [Flow]. + * + * Created via `SimpleBuilder.withQuery { flow }`. The store follows the supplied query flow and + * exposes no query API of its own. + * + * @param Q the type representing the query. + * @param T the type of data held by the store. + */ +public interface SimpleExternalQueryBuilder : BaseBuilder> { + + /** + * Transitions the builder to a variant that supports a suspending (one-shot) local storage + * layer, preserving the configured external query flow. + */ + public fun addSuspendingLocalStorage(): SimpleExternalQuerySuspendingBuilder + + /** + * Transitions the builder to a variant that supports a reactive (Flow-based) local storage + * layer, preserving the configured external query flow. + */ + public fun addReactiveLocalStorage(): SimpleExternalQueryReactiveBuilder + + /** + * Configures a fetcher-less variant that observes only local reactive data, preserving the + * configured external query flow. + */ + public fun disableFetcher(): SimpleExternalQueryReactiveNoFetcherBuilder + + /** + * Transitions the builder into a keyed variant. The configured external query flow is shared + * by every key (each key follows the same query stream). For per-key query flows, call + * `withKeys()` before `withQuery { key -> ... }` instead. + * + * @param Key the type of the keys managed by the resulting store. + */ + public fun withKeys(): SimpleKeyedExternalQueryBuilder + + /** + * Builds a [SimpleStore] using the provided [SimpleQueryContract] implementation. + */ + public fun build(contract: SimpleQueryContract): SimpleStore + + /** + * Builds a [SimpleStore] using a single lambda that fetches remote data for the current query [Q]. + */ + public fun build(onFetch: suspend (Q) -> T): SimpleStore + + /** + * Builds a [SimpleStore] with a custom loader that emits values manually through an [Emitter], + * receiving the current query [Q]. + */ + public fun buildCustom(loader: suspend Emitter.(Q) -> Unit): SimpleStore +} + +/** + * Builder for a [SimpleStore] backed by suspending (one-shot) local storage whose query is driven + * by an external [Flow]. Created via `SimpleSuspendingBuilder.withQuery { flow }`. + * + * @param Q the type representing the query. + * @param T the type of data held by the store. + */ +public interface SimpleExternalQuerySuspendingBuilder : + BaseBuilder> { + + /** + * Transitions the builder into a keyed variant with suspending local storage. The configured + * external query flow is shared by every key. + * + * @param Key the type of the keys managed by the resulting store. + */ + public fun withKeys(): SimpleKeyedExternalQuerySuspendingBuilder + + /** + * Builds a [SimpleStore] using the provided [SimpleQuerySuspendingContract] implementation. + */ + public fun build(contract: SimpleQuerySuspendingContract): SimpleStore + + /** + * Builds a [SimpleStore] using individual lambdas for remote fetch and suspending local storage. + */ + public fun build( + onFetch: suspend (Q) -> T, + onSaveToStorage: suspend (Q, T) -> Unit, + onLoadFromStorage: suspend (Q) -> T?, + ): SimpleStore +} + +/** + * Builder for a [SimpleStore] backed by reactive (Flow-based) local storage whose query is driven + * by an external [Flow]. Created via `SimpleReactiveBuilder.withQuery { flow }`. + * + * @param Q the type representing the query. + * @param T the type of data held by the store. + */ +public interface SimpleExternalQueryReactiveBuilder : + BaseBuilder> { + + /** + * Configures a fetcher-less variant that observes only local reactive data, preserving the + * configured external query flow. + */ + public fun disableFetcher(): SimpleExternalQueryReactiveNoFetcherBuilder + + /** + * Transitions the builder into a keyed variant with reactive local storage. The configured + * external query flow is shared by every key. + * + * @param Key the type of the keys managed by the resulting store. + */ + public fun withKeys(): SimpleKeyedExternalQueryReactiveBuilder + + /** + * Builds a [SimpleStore] using the provided [SimpleQueryReactiveContract] implementation. + */ + public fun build(contract: SimpleQueryReactiveContract): SimpleStore + + /** + * Builds a [SimpleStore] using individual lambdas for remote fetch and reactive local storage. + */ + public fun build( + onFetch: suspend (Q) -> T, + onSaveToStorage: suspend (Q, T) -> Unit, + onObserveStorage: (Q) -> Flow, + ): SimpleStore +} + +/** + * Builder for a fetcher-less [SimpleStore] that observes local data reactively, parameterized by a + * query driven by an external [Flow]. Created via `SimpleReactiveNoFetcherBuilder.withQuery { flow }`. + * + * @param Q the type representing the query. + * @param T the type of data held by the store. + */ +public interface SimpleExternalQueryReactiveNoFetcherBuilder : + BaseBuilder> { + + /** + * Transitions the builder into a keyed, fetcher-less variant. The configured external query + * flow is shared by every key. + * + * @param Key the type of the keys managed by the resulting store. + */ + public fun withKeys(): SimpleKeyedExternalQueryReactiveNoFetcherBuilder + + /** + * Builds a [SimpleStore] using the provided [SimpleQueryReactiveNoFetcherContract] implementation. + */ + public fun build(contract: SimpleQueryReactiveNoFetcherContract): SimpleStore + + /** + * Builds a [SimpleStore] using only a lambda for observing local data for the current query [Q]. + */ + public fun build(onObserve: (Q) -> Flow): SimpleStore +} diff --git a/store/src/main/java/com/elveum/store/builders/SimpleKeyedExternalQueryStoreBuilders.kt b/store/src/main/java/com/elveum/store/builders/SimpleKeyedExternalQueryStoreBuilders.kt new file mode 100644 index 0000000..5e4988b --- /dev/null +++ b/store/src/main/java/com/elveum/store/builders/SimpleKeyedExternalQueryStoreBuilders.kt @@ -0,0 +1,140 @@ +package com.elveum.store.builders + +import com.elveum.container.Emitter +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleKeyedQueryContract +import com.elveum.store.contracts.SimpleKeyedQueryReactiveContract +import com.elveum.store.contracts.SimpleKeyedQueryReactiveNoFetcherContract +import com.elveum.store.contracts.SimpleKeyedQuerySuspendingContract +import com.elveum.store.stores.keyed.KeyedStore +import kotlinx.coroutines.flow.Flow + +/** + * Builder for a remote-only keyed [KeyedStore] whose per-key query is driven by an external [Flow]. + * + * Created via `SimpleKeyedBuilder.withQuery { key -> flow }`. Each key follows its own query flow; + * the resulting store exposes no query API of its own. + * + * @param Key the type of the keys managed by the store. + * @param Q the type representing the query. + * @param T the type of value cached per key. + */ +public interface SimpleKeyedExternalQueryBuilder : + BaseBuilder> { + + /** + * Transitions the builder to a variant that supports a suspending (one-shot) local storage + * layer, preserving the configured per-key external query flow. + */ + public fun addSuspendingLocalStorage(): SimpleKeyedExternalQuerySuspendingBuilder + + /** + * Transitions the builder to a variant that supports a reactive (Flow-based) local storage + * layer, preserving the configured per-key external query flow. + */ + public fun addReactiveLocalStorage(): SimpleKeyedExternalQueryReactiveBuilder + + /** + * Configures a fetcher-less variant that observes only local reactive data per key, preserving + * the configured per-key external query flow. + */ + public fun disableFetcher(): SimpleKeyedExternalQueryReactiveNoFetcherBuilder + + /** + * Builds a [KeyedStore] using the provided [SimpleKeyedQueryContract] implementation. + */ + public fun build(contract: SimpleKeyedQueryContract): KeyedStore + + /** + * Builds a [KeyedStore] using a single lambda that fetches remote data for a key and query. + */ + public fun build(onFetch: suspend (Key, Q) -> T): KeyedStore + + /** + * Builds a [KeyedStore] with a custom loader that emits values manually through an [Emitter], + * receiving the key and the current query [Q]. + */ + public fun buildCustom(loader: suspend Emitter.(Key, Q) -> Unit): KeyedStore +} + +/** + * Builder for a keyed [KeyedStore] backed by suspending local storage whose per-key query is + * driven by an external [Flow]. Created via `SimpleKeyedSuspendingBuilder.withQuery { key -> flow }`. + * + * @param Key the type of the keys managed by the store. + * @param Q the type representing the query. + * @param T the type of value cached per key. + */ +public interface SimpleKeyedExternalQuerySuspendingBuilder : + BaseBuilder> { + + /** + * Builds a [KeyedStore] using the provided [SimpleKeyedQuerySuspendingContract] implementation. + */ + public fun build(contract: SimpleKeyedQuerySuspendingContract): KeyedStore + + /** + * Builds a [KeyedStore] using individual lambdas for remote fetch and suspending local storage. + */ + public fun build( + onFetch: suspend (Key, Q) -> T, + onSaveToStorage: suspend (Key, Q, T) -> Unit, + onLoadFromStorage: suspend (Key, Q) -> T?, + ): KeyedStore +} + +/** + * Builder for a keyed [KeyedStore] backed by reactive local storage whose per-key query is driven + * by an external [Flow]. Created via `SimpleKeyedReactiveBuilder.withQuery { key -> flow }`. + * + * @param Key the type of the keys managed by the store. + * @param Q the type representing the query. + * @param T the type of value cached per key. + */ +public interface SimpleKeyedExternalQueryReactiveBuilder : + BaseBuilder> { + + /** + * Configures a fetcher-less variant that observes only local reactive data per key, preserving + * the configured per-key external query flow. + */ + public fun disableFetcher(): SimpleKeyedExternalQueryReactiveNoFetcherBuilder + + /** + * Builds a [KeyedStore] using the provided [SimpleKeyedQueryReactiveContract] implementation. + */ + public fun build(contract: SimpleKeyedQueryReactiveContract): KeyedStore + + /** + * Builds a [KeyedStore] using individual lambdas for remote fetch and reactive local storage. + */ + public fun build( + onFetch: suspend (Key, Q) -> T, + onSaveToStorage: suspend (Key, Q, T) -> Unit, + onObserveStorage: (Key, Q) -> Flow, + ): KeyedStore +} + +/** + * Builder for a fetcher-less keyed [KeyedStore] that observes local data reactively per key, + * parameterized by a per-key query driven by an external [Flow]. Created via + * `SimpleKeyedReactiveNoFetcherBuilder.withQuery { key -> flow }`. + * + * @param Key the type of the keys managed by the store. + * @param Q the type representing the query. + * @param T the type of value cached per key. + */ +public interface SimpleKeyedExternalQueryReactiveNoFetcherBuilder : + BaseBuilder> { + + /** + * Builds a [KeyedStore] using the provided [SimpleKeyedQueryReactiveNoFetcherContract] + * implementation. + */ + public fun build(contract: SimpleKeyedQueryReactiveNoFetcherContract): KeyedStore + + /** + * Builds a [KeyedStore] using only a lambda for observing local data for a key and query. + */ + public fun build(onObserve: (Key, Q) -> Flow): KeyedStore +} diff --git a/store/src/main/java/com/elveum/store/builders/SimpleKeyedStoreBuilders.kt b/store/src/main/java/com/elveum/store/builders/SimpleKeyedStoreBuilders.kt index fe356bb..9f39bcb 100644 --- a/store/src/main/java/com/elveum/store/builders/SimpleKeyedStoreBuilders.kt +++ b/store/src/main/java/com/elveum/store/builders/SimpleKeyedStoreBuilders.kt @@ -13,6 +13,7 @@ import com.elveum.store.contracts.SimpleKeyedSuspendingContract import com.elveum.store.stores.keyed.KeyedQueryStore import com.elveum.store.stores.keyed.KeyedStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** * Builder for creating a keyed [KeyedStore] without local storage (remote-only). @@ -39,6 +40,38 @@ public interface SimpleKeyedBuilder : BaseBuilder withQuery(initialQuery: Q, debounceMillis: Long = 0): SimpleKeyedQueryBuilder + /** + * Transitions the builder to a variant whose per-key query is driven by an external [Flow]. + * Each key performs an immediate first load using [initialQuery], then reloads whenever the + * flow returned by [queryFlow] for that key emits a new query. The resulting store exposes no + * query API. + * + * @param Q the type representing the query. + * @param initialQuery the query each key uses for its immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow] for a given key. + * @return a [SimpleKeyedExternalQueryBuilder] driven by the given per-key query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: (Key) -> Flow, + ): SimpleKeyedExternalQueryBuilder + + /** + * Convenience overload for a [StateFlow] query source: each key's initial query is derived from + * that key's [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow] for a given key. + * @return a [SimpleKeyedExternalQueryBuilder] driven by the given per-key query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: (Key) -> StateFlow, + ): SimpleKeyedExternalQueryBuilder + /** * Configure a keyed store without a fetcher. In this case, each key manages only * local data via a reactive flow (for example, Room or DataStore returning a `Flow` @@ -175,6 +208,38 @@ public interface SimpleKeyedReactiveBuilder : debounceMillis: Long = 0, ): SimpleKeyedQueryReactiveBuilder + /** + * Transitions the builder to a variant whose per-key query is driven by an external [Flow], + * backed by reactive local storage. Each key performs an immediate first load using + * [initialQuery], then reloads whenever [queryFlow] for that key emits a new query. The + * resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query each key uses for its immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow] for a given key. + * @return a [SimpleKeyedExternalQueryReactiveBuilder] driven by the given per-key query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: (Key) -> Flow, + ): SimpleKeyedExternalQueryReactiveBuilder + + /** + * Convenience overload for a [StateFlow] query source: each key's initial query is derived from + * that key's [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow] for a given key. + * @return a [SimpleKeyedExternalQueryReactiveBuilder] driven by the given per-key query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: (Key) -> StateFlow, + ): SimpleKeyedExternalQueryReactiveBuilder + /** * Configure a keyed store without a fetcher; each key manages only local data via a * reactive flow. @@ -231,6 +296,38 @@ public interface SimpleKeyedReactiveNoFetcherBuilder : debounceMillis: Long = 0, ): SimpleKeyedQueryReactiveNoFetcherBuilder + /** + * Transitions the builder to a fetcher-less variant whose per-key query is driven by an + * external [Flow]. Each key performs an immediate first observe using [initialQuery], then + * re-observes whenever [queryFlow] for that key emits a new query. The resulting store exposes + * no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query each key uses for its immediate first observe. + * @param debounceMillis debounce applied to flow emissions before re-observing; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow] for a given key. + * @return a [SimpleKeyedExternalQueryReactiveNoFetcherBuilder] driven by the given per-key query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: (Key) -> Flow, + ): SimpleKeyedExternalQueryReactiveNoFetcherBuilder + + /** + * Convenience overload for a [StateFlow] query source: each key's initial query is derived from + * that key's [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before re-observing; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow] for a given key. + * @return a [SimpleKeyedExternalQueryReactiveNoFetcherBuilder] driven by the given per-key query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: (Key) -> StateFlow, + ): SimpleKeyedExternalQueryReactiveNoFetcherBuilder + /** * Builds a [KeyedStore] using only a lambda for observing local data per key (no remote fetches). * @@ -271,6 +368,38 @@ public interface SimpleKeyedSuspendingBuilder : debounceMillis: Long = 0, ): SimpleKeyedQuerySuspendingBuilder + /** + * Transitions the builder to a variant whose per-key query is driven by an external [Flow], + * backed by suspending local storage. Each key performs an immediate first load using + * [initialQuery], then reloads whenever [queryFlow] for that key emits a new query. The + * resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query each key uses for its immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow] for a given key. + * @return a [SimpleKeyedExternalQuerySuspendingBuilder] driven by the given per-key query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: (Key) -> Flow, + ): SimpleKeyedExternalQuerySuspendingBuilder + + /** + * Convenience overload for a [StateFlow] query source: each key's initial query is derived from + * that key's [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow] for a given key. + * @return a [SimpleKeyedExternalQuerySuspendingBuilder] driven by the given per-key query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: (Key) -> StateFlow, + ): SimpleKeyedExternalQuerySuspendingBuilder + /** * Builds a [KeyedStore] using individual lambdas for remote fetch and suspending local storage. * diff --git a/store/src/main/java/com/elveum/store/builders/SimpleStoreBuilders.kt b/store/src/main/java/com/elveum/store/builders/SimpleStoreBuilders.kt index 6cdad11..a4eb4ae 100644 --- a/store/src/main/java/com/elveum/store/builders/SimpleStoreBuilders.kt +++ b/store/src/main/java/com/elveum/store/builders/SimpleStoreBuilders.kt @@ -13,6 +13,7 @@ import com.elveum.store.contracts.SimpleSuspendingContract import com.elveum.store.stores.simple.SimpleQueryStore import com.elveum.store.stores.simple.SimpleStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlin.reflect.KClass /** @@ -38,6 +39,37 @@ public interface SimpleBuilder : BaseBuilder> { */ public fun withQuery(initialQuery: Q, debounceMillis: Long = 0): SimpleQueryBuilder + /** + * Transitions the builder to a variant whose query is driven by an external [Flow]. + * The store performs an immediate first load using [initialQuery], then reloads whenever the + * flow returned by [queryFlow] emits a new query. The resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query used for the immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow]. + * @return a [SimpleExternalQueryBuilder] driven by the given query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: () -> Flow, + ): SimpleExternalQueryBuilder + + /** + * Convenience overload for a [StateFlow] query source: the initial query is derived from + * [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow]. + * @return a [SimpleExternalQueryBuilder] driven by the given query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: () -> StateFlow, + ): SimpleExternalQueryBuilder + /** * Transitions the builder into a keyed variant: instead of a single value, the * resulting store manages one value per key (like a map of independently-cached @@ -181,6 +213,37 @@ public interface SimpleSuspendingBuilder : BaseBuilder withQuery(initialQuery: Q, debounceMillis: Long = 0): SimpleQuerySuspendingBuilder + /** + * Transitions the builder to a variant whose query is driven by an external [Flow], backed by + * suspending local storage. The store performs an immediate first load using [initialQuery], + * then reloads whenever [queryFlow] emits a new query. The resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query used for the immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow]. + * @return a [SimpleExternalQuerySuspendingBuilder] driven by the given query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: () -> Flow, + ): SimpleExternalQuerySuspendingBuilder + + /** + * Convenience overload for a [StateFlow] query source: the initial query is derived from + * [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow]. + * @return a [SimpleExternalQuerySuspendingBuilder] driven by the given query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: () -> StateFlow, + ): SimpleExternalQuerySuspendingBuilder + /** * Transitions the builder into a keyed variant with suspending local storage that * manages one value per key. See [SimpleKeyedSuspendingBuilder]. @@ -232,6 +295,37 @@ public interface SimpleReactiveBuilder : BaseBuilder withQuery(initialQuery: Q, debounceMillis: Long = 0): SimpleQueryReactiveBuilder + /** + * Transitions the builder to a variant whose query is driven by an external [Flow], backed by + * reactive local storage. The store performs an immediate first load using [initialQuery], + * then reloads whenever [queryFlow] emits a new query. The resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query used for the immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow]. + * @return a [SimpleExternalQueryReactiveBuilder] driven by the given query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: () -> Flow, + ): SimpleExternalQueryReactiveBuilder + + /** + * Convenience overload for a [StateFlow] query source: the initial query is derived from + * [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow]. + * @return a [SimpleExternalQueryReactiveBuilder] driven by the given query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: () -> StateFlow, + ): SimpleExternalQueryReactiveBuilder + /** * Configure a simple store without fetcher. In this case, the store manages only * local data via reactive flow (for example, Room or DataStore returning Flow of items). @@ -380,6 +474,37 @@ public interface SimpleReactiveNoFetcherBuilder : BaseBuilder withQuery(initialQuery: Q, debounceMillis: Long = 0): SimpleQueryReactiveNoFetcherBuilder + /** + * Transitions the builder to a fetcher-less variant whose query is driven by an external [Flow]. + * The store performs an immediate first load using [initialQuery], then reloads whenever + * [queryFlow] emits a new query. The resulting store exposes no query API. + * + * @param Q the type representing the query. + * @param initialQuery the query used for the immediate first load. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [Flow]. + * @return a [SimpleExternalQueryReactiveNoFetcherBuilder] driven by the given query flow. + */ + public fun withQuery( + initialQuery: Q, + debounceMillis: Long = 0, + queryFlow: () -> Flow, + ): SimpleExternalQueryReactiveNoFetcherBuilder + + /** + * Convenience overload for a [StateFlow] query source: the initial query is derived from + * [StateFlow.value], so no explicit initial query is required. + * + * @param Q the type representing the query. + * @param debounceMillis debounce applied to flow emissions before reloading; defaults to `0`. + * @param queryFlow lambda returning the external query [StateFlow]. + * @return a [SimpleExternalQueryReactiveNoFetcherBuilder] driven by the given query state flow. + */ + public fun withQuery( + debounceMillis: Long = 0, + queryFlow: () -> StateFlow, + ): SimpleExternalQueryReactiveNoFetcherBuilder + /** * Transitions the builder into a keyed, fetcher-less variant that manages one * locally-observed value per key. See [SimpleKeyedReactiveNoFetcherBuilder]. diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedBuilderImpl.kt index dbb29c5..4ca8bdb 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedBuilderImpl.kt @@ -3,6 +3,7 @@ package com.elveum.store.internal.builders.keyed import com.elveum.container.subject.paging.PageEmitter import com.elveum.store.builders.BasePagedBuilder import com.elveum.store.builders.PagedKeyedBuilder +import com.elveum.store.builders.PagedKeyedExternalQueryBuilder import com.elveum.store.builders.PagedKeyedQueryBuilder import com.elveum.store.builders.PagedKeyedSuspendingBuilder import com.elveum.store.contracts.PagedKeyedContract @@ -12,6 +13,8 @@ import com.elveum.store.internal.stores.PagedKeyedQueryStoreImpl import com.elveum.store.internal.stores.common.CorePageFetcher import com.elveum.store.stores.keyed.PagedKeyedStore import com.elveum.store.stores.paged.PagedList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow internal class PagedKeyedBuilderImpl( val config: SharedPageConfig, @@ -30,6 +33,31 @@ internal class PagedKeyedBuilderImpl( return PagedKeyedQueryBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: (Key) -> Flow, + ): PagedKeyedExternalQueryBuilder { + return PagedKeyedExternalQueryBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: (Key) -> StateFlow, + ): PagedKeyedExternalQueryBuilder { + return PagedKeyedExternalQueryBuilderImpl( + initialQueryProvider = { key -> queryFlow(key).value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun addSuspendingLocalStorage(): PagedKeyedSuspendingBuilder { return PagedKeyedSuspendingBuilderImpl(config) } @@ -41,7 +69,7 @@ internal class PagedKeyedBuilderImpl( override fun build(onFetch: suspend (Key, PageKey) -> PagedList): PagedKeyedStore { return PagedKeyedQueryStoreImpl( config = config, - initialQuery = Unit, + initialQueryProvider = { Unit }, fetcher = { key, _, pageKey -> onFetch(key, pageKey) }, ) } @@ -49,7 +77,7 @@ internal class PagedKeyedBuilderImpl( override fun buildCustom(loader: suspend PageEmitter.(Key, PageKey) -> Unit): PagedKeyedStore { return PagedKeyedQueryStoreImpl( config = config, - initialQuery = Unit, + initialQueryProvider = { Unit }, fetcher = CorePageFetcher.Custom { key, _, pageKey -> loader(key, pageKey) }, ) } diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedExternalQueryBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedExternalQueryBuilderImpl.kt new file mode 100644 index 0000000..ad5873b --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedExternalQueryBuilderImpl.kt @@ -0,0 +1,61 @@ +package com.elveum.store.internal.builders.keyed + +import com.elveum.container.subject.paging.PageEmitter +import com.elveum.store.builders.BasePagedBuilder +import com.elveum.store.builders.PagedKeyedExternalQueryBuilder +import com.elveum.store.builders.PagedKeyedExternalQuerySuspendingBuilder +import com.elveum.store.contracts.PagedKeyedQueryContract +import com.elveum.store.internal.builders.paged.BasePageBuilderImpl +import com.elveum.store.internal.builders.paged.SharedPageConfig +import com.elveum.store.internal.stores.PagedKeyedQueryStoreImpl +import com.elveum.store.internal.stores.common.CorePageFetcher +import com.elveum.store.stores.keyed.PagedKeyedStore +import com.elveum.store.stores.paged.PagedList +import kotlinx.coroutines.flow.Flow + +internal class PagedKeyedExternalQueryBuilderImpl( + private val initialQueryProvider: (Key) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: (Key) -> Flow, + private val config: SharedPageConfig, + baseBuilder: BasePageBuilderImpl> = + BasePageBuilderImpl(config), +) : PagedKeyedExternalQueryBuilder, + BasePagedBuilder> by baseBuilder { + + init { + baseBuilder.setReference(this) + } + + override fun addSuspendingLocalStorage(): PagedKeyedExternalQuerySuspendingBuilder { + return PagedKeyedExternalQuerySuspendingBuilderImpl( + initialQueryProvider, queryDebounceMillis, queryFlow, config, + ) + } + + override fun build(contract: PagedKeyedQueryContract): PagedKeyedStore { + return build(onFetch = contract::fetch) + } + + override fun build(onFetch: suspend (Key, Q, PageKey) -> PagedList): PagedKeyedStore { + return PagedKeyedQueryStoreImpl( + config = config, + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + fetcher = onFetch, + externalQueryProvider = queryFlow, + ) + } + + override fun buildCustom( + loader: suspend PageEmitter.(Key, Q, PageKey) -> Unit, + ): PagedKeyedStore { + return PagedKeyedQueryStoreImpl( + config = config, + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + fetcher = CorePageFetcher.Custom { key, query, pageKey -> loader(key, query, pageKey) }, + externalQueryProvider = queryFlow, + ) + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedExternalQuerySuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedExternalQuerySuspendingBuilderImpl.kt new file mode 100644 index 0000000..9982261 --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedExternalQuerySuspendingBuilderImpl.kt @@ -0,0 +1,52 @@ +package com.elveum.store.internal.builders.keyed + +import com.elveum.store.builders.BasePagedBuilder +import com.elveum.store.builders.PagedKeyedExternalQuerySuspendingBuilder +import com.elveum.store.contracts.PagedKeyedQuerySuspendingContract +import com.elveum.store.internal.builders.paged.BasePageBuilderImpl +import com.elveum.store.internal.builders.paged.SharedPageConfig +import com.elveum.store.internal.stores.PagedKeyedQueryStoreImpl +import com.elveum.store.stores.keyed.PagedKeyedStore +import com.elveum.store.stores.paged.PagedList +import kotlinx.coroutines.flow.Flow + +internal class PagedKeyedExternalQuerySuspendingBuilderImpl( + private val initialQueryProvider: (Key) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: (Key) -> Flow, + private val config: SharedPageConfig, + baseBuilder: BasePageBuilderImpl> = + BasePageBuilderImpl(config), +) : PagedKeyedExternalQuerySuspendingBuilder, + BasePagedBuilder> by baseBuilder { + + init { + baseBuilder.setReference(this) + } + + override fun build( + contract: PagedKeyedQuerySuspendingContract, + ): PagedKeyedStore { + return build( + onFetch = contract::fetch, + onSaveToStorage = contract::saveToLocalStorage, + onLoadFromStorage = contract::loadFromLocalStorage, + ) + } + + override fun build( + onFetch: suspend (Key, Q, PageKey) -> PagedList, + onSaveToStorage: suspend (Key, Q, PageKey, PagedList) -> Unit, + onLoadFromStorage: suspend (Key, Q, PageKey) -> PagedList?, + ): PagedKeyedStore { + return PagedKeyedQueryStoreImpl( + fetcher = onFetch, + loader = onLoadFromStorage, + saver = onSaveToStorage, + config = config, + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + externalQueryProvider = queryFlow, + ) + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedQueryBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedQueryBuilderImpl.kt index 3fde0ae..ba9e484 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedQueryBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedQueryBuilderImpl.kt @@ -36,7 +36,7 @@ internal class PagedKeyedQueryBuilderImpl PagedList): PagedKeyedQueryStore { return PagedKeyedQueryStoreImpl( config = config, - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, fetcher = onFetch, ) @@ -47,7 +47,7 @@ internal class PagedKeyedQueryBuilderImpl { return PagedKeyedQueryStoreImpl( config = config, - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, fetcher = CorePageFetcher.Custom { key, query, pageKey -> loader(key, query, pageKey) }, ) diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedQuerySuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedQuerySuspendingBuilderImpl.kt index 0af1240..731fabc 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedQuerySuspendingBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/PagedKeyedQuerySuspendingBuilderImpl.kt @@ -42,7 +42,7 @@ internal class PagedKeyedQuerySuspendingBuilderImpl( val config: SharedPageConfig, @@ -28,6 +31,31 @@ internal class PagedKeyedSuspendingBuilderImpl withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: (Key) -> Flow, + ): PagedKeyedExternalQuerySuspendingBuilder { + return PagedKeyedExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: (Key) -> StateFlow, + ): PagedKeyedExternalQuerySuspendingBuilder { + return PagedKeyedExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { key -> queryFlow(key).value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun build(contract: PagedKeyedSuspendingContract): PagedKeyedStore { return build( onFetch = contract::fetch, @@ -46,7 +74,7 @@ internal class PagedKeyedSuspendingBuilderImpl onLoadFromStorage(key, pageKey) }, saver = { key, _, pageKey, pagedList -> onSaveToStorage(key, pageKey, pagedList) }, config = config, - initialQuery = Unit, + initialQueryProvider = { Unit }, ) } } diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedBuilderImpl.kt index 5918798..abafea1 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedBuilderImpl.kt @@ -2,6 +2,7 @@ package com.elveum.store.internal.builders.keyed import com.elveum.container.Emitter import com.elveum.store.builders.SimpleKeyedBuilder +import com.elveum.store.builders.SimpleKeyedExternalQueryBuilder import com.elveum.store.builders.SimpleKeyedQueryBuilder import com.elveum.store.builders.SimpleKeyedReactiveBuilder import com.elveum.store.builders.SimpleKeyedReactiveNoFetcherBuilder @@ -14,6 +15,8 @@ import com.elveum.store.internal.stores.KeyedQueryStoreImpl import com.elveum.store.internal.stores.common.CoreFetcher import com.elveum.store.stores.keyed.KeyedStore import com.elveum.store.stores.simple.SimpleStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow internal class SimpleKeyedBuilderImpl( val config: SharedConfig, @@ -40,6 +43,31 @@ internal class SimpleKeyedBuilderImpl( return SimpleKeyedQueryBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: (Key) -> Flow, + ): SimpleKeyedExternalQueryBuilder { + return SimpleKeyedExternalQueryBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: (Key) -> StateFlow, + ): SimpleKeyedExternalQueryBuilder { + return SimpleKeyedExternalQueryBuilderImpl( + initialQueryProvider = { key -> queryFlow(key).value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun build(contract: SimpleKeyedContract): KeyedStore { return build(onFetch = contract::fetch) } @@ -48,7 +76,7 @@ internal class SimpleKeyedBuilderImpl( return KeyedQueryStoreImpl( config = config, fetcher = { key, _ -> onFetch(key) }, - initialQuery = Unit, + initialQueryProvider = { Unit }, ) } @@ -56,7 +84,7 @@ internal class SimpleKeyedBuilderImpl( return KeyedQueryStoreImpl( config = config, fetcher = CoreFetcher.Custom { key, _ -> loader(key) }, - initialQuery = Unit, + initialQueryProvider = { Unit }, ) } } diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryBuilderImpl.kt new file mode 100644 index 0000000..1674283 --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryBuilderImpl.kt @@ -0,0 +1,71 @@ +package com.elveum.store.internal.builders.keyed + +import com.elveum.container.Emitter +import com.elveum.store.builders.SimpleKeyedExternalQueryBuilder +import com.elveum.store.builders.SimpleKeyedExternalQueryReactiveBuilder +import com.elveum.store.builders.SimpleKeyedExternalQueryReactiveNoFetcherBuilder +import com.elveum.store.builders.SimpleKeyedExternalQuerySuspendingBuilder +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleKeyedQueryContract +import com.elveum.store.internal.builders.BaseBuilderImpl +import com.elveum.store.internal.builders.SharedConfig +import com.elveum.store.internal.stores.KeyedQueryStoreImpl +import com.elveum.store.internal.stores.common.CoreFetcher +import com.elveum.store.stores.keyed.KeyedStore +import kotlinx.coroutines.flow.Flow + +internal class SimpleKeyedExternalQueryBuilderImpl( + private val initialQueryProvider: (Key) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: (Key) -> Flow, + private val config: SharedConfig, + sharedBuilder: BaseBuilderImpl> = BaseBuilderImpl(config), +) : SimpleKeyedExternalQueryBuilder, + BaseBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + override fun addSuspendingLocalStorage(): SimpleKeyedExternalQuerySuspendingBuilder { + return SimpleKeyedExternalQuerySuspendingBuilderImpl( + initialQueryProvider, queryDebounceMillis, queryFlow, config, + ) + } + + override fun addReactiveLocalStorage(): SimpleKeyedExternalQueryReactiveBuilder { + return SimpleKeyedExternalQueryReactiveBuilderImpl( + initialQueryProvider, queryDebounceMillis, queryFlow, config, + ) + } + + override fun disableFetcher(): SimpleKeyedExternalQueryReactiveNoFetcherBuilder { + return SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider, queryDebounceMillis, queryFlow, config, + ) + } + + override fun build(contract: SimpleKeyedQueryContract): KeyedStore { + return build(onFetch = contract::fetch) + } + + override fun build(onFetch: suspend (Key, Q) -> T): KeyedStore { + return KeyedQueryStoreImpl( + fetcher = onFetch, + config = config, + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + externalQueryProvider = queryFlow, + ) + } + + override fun buildCustom(loader: suspend Emitter.(Key, Q) -> Unit): KeyedStore { + return KeyedQueryStoreImpl( + fetcher = CoreFetcher.Custom { key, query -> loader(key, query) }, + config = config, + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + externalQueryProvider = queryFlow, + ) + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryReactiveBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryReactiveBuilderImpl.kt new file mode 100644 index 0000000..282b1d8 --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryReactiveBuilderImpl.kt @@ -0,0 +1,55 @@ +package com.elveum.store.internal.builders.keyed + +import com.elveum.store.builders.SimpleKeyedExternalQueryReactiveBuilder +import com.elveum.store.builders.SimpleKeyedExternalQueryReactiveNoFetcherBuilder +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleKeyedQueryReactiveContract +import com.elveum.store.internal.builders.BaseBuilderImpl +import com.elveum.store.internal.builders.SharedConfig +import com.elveum.store.internal.stores.KeyedQueryStoreImpl +import com.elveum.store.stores.keyed.KeyedStore +import kotlinx.coroutines.flow.Flow + +internal class SimpleKeyedExternalQueryReactiveBuilderImpl( + private val initialQueryProvider: (Key) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: (Key) -> Flow, + private val config: SharedConfig, + sharedBuilder: BaseBuilderImpl> = BaseBuilderImpl(config), +) : SimpleKeyedExternalQueryReactiveBuilder, + BaseBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + override fun disableFetcher(): SimpleKeyedExternalQueryReactiveNoFetcherBuilder { + return SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider, queryDebounceMillis, queryFlow, config, + ) + } + + override fun build(contract: SimpleKeyedQueryReactiveContract): KeyedStore { + return build( + onFetch = contract::fetch, + onSaveToStorage = contract::saveToLocalStorage, + onObserveStorage = contract::observeLocalStorage, + ) + } + + override fun build( + onFetch: suspend (Key, Q) -> T, + onSaveToStorage: suspend (Key, Q, T) -> Unit, + onObserveStorage: (Key, Q) -> Flow, + ): KeyedStore { + return KeyedQueryStoreImpl( + fetcher = onFetch, + saver = onSaveToStorage, + observer = onObserveStorage, + config = config, + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + externalQueryProvider = queryFlow, + ) + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl.kt new file mode 100644 index 0000000..794d1c0 --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl.kt @@ -0,0 +1,42 @@ +package com.elveum.store.internal.builders.keyed + +import com.elveum.store.builders.SimpleKeyedExternalQueryReactiveNoFetcherBuilder +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleKeyedQueryReactiveNoFetcherContract +import com.elveum.store.internal.builders.BaseBuilderImpl +import com.elveum.store.internal.builders.SharedConfig +import com.elveum.store.internal.stores.KeyedQueryStoreImpl +import com.elveum.store.stores.keyed.KeyedStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +internal class SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl( + private val initialQueryProvider: (Key) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: (Key) -> Flow, + private val config: SharedConfig, + sharedBuilder: BaseBuilderImpl> = + BaseBuilderImpl(config), +) : SimpleKeyedExternalQueryReactiveNoFetcherBuilder, + BaseBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + override fun build(contract: SimpleKeyedQueryReactiveNoFetcherContract): KeyedStore { + return build(onObserve = contract::observe) + } + + override fun build(onObserve: (Key, Q) -> Flow): KeyedStore { + return KeyedQueryStoreImpl( + fetcher = { key, query -> onObserve(key, query).first() }, + saver = { _, _, _ -> }, + observer = { key, query -> onObserve(key, query) }, + config = config, + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + externalQueryProvider = queryFlow, + ) + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQuerySuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQuerySuspendingBuilderImpl.kt new file mode 100644 index 0000000..f736b38 --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedExternalQuerySuspendingBuilderImpl.kt @@ -0,0 +1,48 @@ +package com.elveum.store.internal.builders.keyed + +import com.elveum.store.builders.SimpleKeyedExternalQuerySuspendingBuilder +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleKeyedQuerySuspendingContract +import com.elveum.store.internal.builders.BaseBuilderImpl +import com.elveum.store.internal.builders.SharedConfig +import com.elveum.store.internal.stores.KeyedQueryStoreImpl +import com.elveum.store.stores.keyed.KeyedStore +import kotlinx.coroutines.flow.Flow + +internal class SimpleKeyedExternalQuerySuspendingBuilderImpl( + private val initialQueryProvider: (Key) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: (Key) -> Flow, + private val config: SharedConfig, + sharedBuilder: BaseBuilderImpl> = BaseBuilderImpl(config), +) : SimpleKeyedExternalQuerySuspendingBuilder, + BaseBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + override fun build(contract: SimpleKeyedQuerySuspendingContract): KeyedStore { + return build( + onFetch = contract::fetch, + onSaveToStorage = contract::saveToLocalStorage, + onLoadFromStorage = contract::loadFromLocalStorage, + ) + } + + override fun build( + onFetch: suspend (Key, Q) -> T, + onSaveToStorage: suspend (Key, Q, T) -> Unit, + onLoadFromStorage: suspend (Key, Q) -> T?, + ): KeyedStore { + return KeyedQueryStoreImpl( + fetcher = onFetch, + loader = onLoadFromStorage, + saver = onSaveToStorage, + config = config, + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + externalQueryProvider = queryFlow, + ) + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryBuilderImpl.kt index 4fcdc37..4ac4217 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryBuilderImpl.kt @@ -47,7 +47,7 @@ internal class SimpleKeyedQueryBuilderImpl( return KeyedQueryStoreImpl( fetcher = onFetch, config = config, - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, ) } @@ -56,7 +56,7 @@ internal class SimpleKeyedQueryBuilderImpl( return KeyedQueryStoreImpl( fetcher = CoreFetcher.Custom { key, query -> loader(key, query) }, config = config, - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, ) } diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryReactiveBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryReactiveBuilderImpl.kt index c58f497..613fc84 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryReactiveBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryReactiveBuilderImpl.kt @@ -36,7 +36,7 @@ internal class SimpleKeyedQueryReactiveBuilderImpl( saver = onSaveToStorage, observer = onObserveStorage, config = config, - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, ) } diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryReactiveNoFetcherBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryReactiveNoFetcherBuilderImpl.kt index de0e8d4..25c9450 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryReactiveNoFetcherBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQueryReactiveNoFetcherBuilderImpl.kt @@ -27,7 +27,7 @@ internal class SimpleKeyedQueryReactiveNoFetcherBuilderImpl onObserve(key, query).first() }, saver = { _, _, _ -> }, observer = { key, query -> onObserve(key, query) }, - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, ) diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQuerySuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQuerySuspendingBuilderImpl.kt index c09229a..de61fbd 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQuerySuspendingBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedQuerySuspendingBuilderImpl.kt @@ -30,7 +30,7 @@ internal class SimpleKeyedQuerySuspendingBuilderImpl( val config: SharedConfig, @@ -31,6 +33,31 @@ internal class SimpleKeyedReactiveBuilderImpl( return SimpleKeyedQueryReactiveBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: (Key) -> Flow, + ): SimpleKeyedExternalQueryReactiveBuilder { + return SimpleKeyedExternalQueryReactiveBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: (Key) -> StateFlow, + ): SimpleKeyedExternalQueryReactiveBuilder { + return SimpleKeyedExternalQueryReactiveBuilderImpl( + initialQueryProvider = { key -> queryFlow(key).value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun build(contract: SimpleKeyedReactiveContract): KeyedStore { return build( onFetch = contract::fetch, @@ -49,7 +76,7 @@ internal class SimpleKeyedReactiveBuilderImpl( fetcher = { key, _ -> onFetch(key) }, saver = { key, _, value -> onSaveToStorage(key, value) }, observer = { key, _ -> onObserveStorage(key) }, - initialQuery = Unit, + initialQueryProvider = { Unit }, ) } } diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedReactiveNoFetcherBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedReactiveNoFetcherBuilderImpl.kt index a53c646..e9c6493 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedReactiveNoFetcherBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedReactiveNoFetcherBuilderImpl.kt @@ -1,5 +1,6 @@ package com.elveum.store.internal.builders.keyed +import com.elveum.store.builders.SimpleKeyedExternalQueryReactiveNoFetcherBuilder import com.elveum.store.builders.SimpleKeyedQueryReactiveNoFetcherBuilder import com.elveum.store.builders.SimpleKeyedReactiveNoFetcherBuilder import com.elveum.store.builders.base.BaseBuilder @@ -9,6 +10,7 @@ import com.elveum.store.internal.builders.SharedConfig import com.elveum.store.internal.stores.KeyedQueryStoreImpl import com.elveum.store.stores.keyed.KeyedStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first internal class SimpleKeyedReactiveNoFetcherBuilderImpl( @@ -28,6 +30,31 @@ internal class SimpleKeyedReactiveNoFetcherBuilderImpl( return SimpleKeyedQueryReactiveNoFetcherBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: (Key) -> Flow, + ): SimpleKeyedExternalQueryReactiveNoFetcherBuilder { + return SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: (Key) -> StateFlow, + ): SimpleKeyedExternalQueryReactiveNoFetcherBuilder { + return SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider = { key -> queryFlow(key).value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun build(contract: SimpleKeyedReactiveNoFetcherContract): KeyedStore { return build(onObserve = contract::observe) } @@ -37,7 +64,7 @@ internal class SimpleKeyedReactiveNoFetcherBuilderImpl( fetcher = { key, _ -> onObserve(key).first() }, saver = { _, _, _ -> }, observer = { key, _ -> onObserve(key) }, - initialQuery = Unit, + initialQueryProvider = { Unit }, config = config, ) } diff --git a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedSuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedSuspendingBuilderImpl.kt index 6e36fa3..9214725 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedSuspendingBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/keyed/SimpleKeyedSuspendingBuilderImpl.kt @@ -1,5 +1,6 @@ package com.elveum.store.internal.builders.keyed +import com.elveum.store.builders.SimpleKeyedExternalQuerySuspendingBuilder import com.elveum.store.builders.SimpleKeyedQuerySuspendingBuilder import com.elveum.store.builders.SimpleKeyedSuspendingBuilder import com.elveum.store.builders.base.BaseBuilder @@ -8,6 +9,8 @@ import com.elveum.store.internal.builders.BaseBuilderImpl import com.elveum.store.internal.builders.SharedConfig import com.elveum.store.internal.stores.KeyedQueryStoreImpl import com.elveum.store.stores.keyed.KeyedStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow internal class SimpleKeyedSuspendingBuilderImpl( private val config: SharedConfig, @@ -25,6 +28,31 @@ internal class SimpleKeyedSuspendingBuilderImpl( return SimpleKeyedQuerySuspendingBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: (Key) -> Flow, + ): SimpleKeyedExternalQuerySuspendingBuilder { + return SimpleKeyedExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: (Key) -> StateFlow, + ): SimpleKeyedExternalQuerySuspendingBuilder { + return SimpleKeyedExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { key -> queryFlow(key).value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun build(contract: SimpleKeyedSuspendingContract): KeyedStore { return build( onFetch = contract::fetch, @@ -43,7 +71,7 @@ internal class SimpleKeyedSuspendingBuilderImpl( fetcher = { key, _ -> onFetch(key) }, saver = { key, _, value -> onSaveToStorage(key, value) }, loader = { key, _ -> onLoadFromStorage(key) }, - initialQuery = Unit, + initialQueryProvider = { Unit }, ) } } diff --git a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedBuilderImpl.kt index 84bed42..4c43816 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedBuilderImpl.kt @@ -3,6 +3,7 @@ package com.elveum.store.internal.builders.paged import com.elveum.container.subject.paging.PageEmitter import com.elveum.store.builders.BasePagedBuilder import com.elveum.store.builders.PagedBuilder +import com.elveum.store.builders.PagedExternalQueryBuilder import com.elveum.store.builders.PagedKeyedBuilder import com.elveum.store.builders.PagedQueryBuilder import com.elveum.store.builders.PagedSuspendingBuilder @@ -13,6 +14,8 @@ import com.elveum.store.internal.stores.asPagedStore import com.elveum.store.internal.stores.common.CorePageFetcher import com.elveum.store.stores.paged.PagedList import com.elveum.store.stores.paged.PagedStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow internal class PagedBuilderImpl

( initialKey: P, @@ -37,6 +40,31 @@ internal class PagedBuilderImpl

( return PagedQueryBuilderImpl(initialQuery, debounceMillis, sharedBuilder.pageConfig) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: () -> Flow, + ): PagedExternalQueryBuilder { + return PagedExternalQueryBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = sharedBuilder.pageConfig, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: () -> StateFlow, + ): PagedExternalQueryBuilder { + return PagedExternalQueryBuilderImpl( + initialQueryProvider = { queryFlow().value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = sharedBuilder.pageConfig, + ) + } + override fun addSuspendingLocalStorage(): PagedSuspendingBuilder { return PagedSuspendingBuilderImpl(sharedBuilder.pageConfig) } @@ -47,7 +75,7 @@ internal class PagedBuilderImpl

( override fun build(onFetch: suspend (P) -> PagedList): PagedStore { return PagedKeyedQueryStoreImpl( - initialQuery = Unit, + initialQueryProvider = { Unit }, config = sharedBuilder.pageConfig, fetcher = { _, _, pageKey -> onFetch(pageKey) }, ).asPagedStore() @@ -55,7 +83,7 @@ internal class PagedBuilderImpl

( override fun buildCustom(loader: suspend PageEmitter.(P) -> Unit): PagedStore { return PagedKeyedQueryStoreImpl( - initialQuery = Unit, + initialQueryProvider = { Unit }, config = sharedBuilder.pageConfig, fetcher = CorePageFetcher.Custom { _, _, pageKey -> loader(pageKey) }, ).asPagedStore() diff --git a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedExternalQueryBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedExternalQueryBuilderImpl.kt new file mode 100644 index 0000000..ab98885 --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedExternalQueryBuilderImpl.kt @@ -0,0 +1,67 @@ +package com.elveum.store.internal.builders.paged + +import com.elveum.container.subject.paging.PageEmitter +import com.elveum.store.builders.BasePagedBuilder +import com.elveum.store.builders.PagedExternalQueryBuilder +import com.elveum.store.builders.PagedExternalQuerySuspendingBuilder +import com.elveum.store.builders.PagedKeyedExternalQueryBuilder +import com.elveum.store.contracts.PagedQueryContract +import com.elveum.store.internal.builders.keyed.PagedKeyedExternalQueryBuilderImpl +import com.elveum.store.internal.stores.PagedKeyedQueryStoreImpl +import com.elveum.store.internal.stores.asPagedStore +import com.elveum.store.internal.stores.common.CorePageFetcher +import com.elveum.store.stores.paged.PagedList +import com.elveum.store.stores.paged.PagedStore +import kotlinx.coroutines.flow.Flow + +internal class PagedExternalQueryBuilderImpl( + private val initialQueryProvider: (Unit) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: () -> Flow, + private val config: SharedPageConfig, + sharedBuilder: BasePageBuilderImpl> = BasePageBuilderImpl(config), +) : PagedExternalQueryBuilder, BasePagedBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + private val externalQueryProvider: (Unit) -> Flow = { queryFlow() } + + override fun withKeys(): PagedKeyedExternalQueryBuilder { + return PagedKeyedExternalQueryBuilderImpl( + initialQueryProvider = { initialQueryProvider(Unit) }, + queryDebounceMillis = queryDebounceMillis, + queryFlow = { queryFlow() }, + config = config, + ) + } + + override fun addSuspendingLocalStorage(): PagedExternalQuerySuspendingBuilder { + return PagedExternalQuerySuspendingBuilderImpl(initialQueryProvider, queryDebounceMillis, queryFlow, config) + } + + override fun build(contract: PagedQueryContract): PagedStore { + return build(onFetch = contract::fetch) + } + + override fun build(onFetch: suspend (Q, P) -> PagedList): PagedStore { + return PagedKeyedQueryStoreImpl( + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + config = config, + fetcher = { _, query, pageKey -> onFetch(query, pageKey) }, + externalQueryProvider = externalQueryProvider, + ).asPagedStore() + } + + override fun buildCustom(loader: suspend PageEmitter.(Q, P) -> Unit): PagedStore { + return PagedKeyedQueryStoreImpl( + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + config = config, + fetcher = CorePageFetcher.Custom { _, query, pageKey -> loader(query, pageKey) }, + externalQueryProvider = externalQueryProvider, + ).asPagedStore() + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedExternalQuerySuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedExternalQuerySuspendingBuilderImpl.kt new file mode 100644 index 0000000..f2a67f9 --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedExternalQuerySuspendingBuilderImpl.kt @@ -0,0 +1,62 @@ +package com.elveum.store.internal.builders.paged + +import com.elveum.store.builders.BasePagedBuilder +import com.elveum.store.builders.PagedExternalQuerySuspendingBuilder +import com.elveum.store.builders.PagedKeyedExternalQuerySuspendingBuilder +import com.elveum.store.contracts.PagedQuerySuspendingContract +import com.elveum.store.internal.builders.keyed.PagedKeyedExternalQuerySuspendingBuilderImpl +import com.elveum.store.internal.stores.PagedKeyedQueryStoreImpl +import com.elveum.store.internal.stores.asPagedStore +import com.elveum.store.stores.paged.PagedList +import com.elveum.store.stores.paged.PagedStore +import kotlinx.coroutines.flow.Flow + +internal class PagedExternalQuerySuspendingBuilderImpl( + private val initialQueryProvider: (Unit) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: () -> Flow, + private val config: SharedPageConfig, + sharedBuilder: BasePageBuilderImpl> = + BasePageBuilderImpl(config), +) : PagedExternalQuerySuspendingBuilder, + BasePagedBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + private val externalQueryProvider: (Unit) -> Flow = { queryFlow() } + + override fun withKeys(): PagedKeyedExternalQuerySuspendingBuilder { + return PagedKeyedExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { initialQueryProvider(Unit) }, + queryDebounceMillis = queryDebounceMillis, + queryFlow = { queryFlow() }, + config = config, + ) + } + + override fun build(contract: PagedQuerySuspendingContract): PagedStore { + return build( + onFetch = contract::fetch, + onSaveToStorage = contract::saveToLocalStorage, + onLoadFromStorage = contract::loadFromLocalStorage, + ) + } + + override fun build( + onFetch: suspend (Q, P) -> PagedList, + onSaveToStorage: suspend (Q, P, PagedList) -> Unit, + onLoadFromStorage: suspend (Q, P) -> PagedList?, + ): PagedStore { + return PagedKeyedQueryStoreImpl( + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + config = config, + fetcher = { _, query, pageKey -> onFetch(query, pageKey) }, + loader = { _, query, pageKey -> onLoadFromStorage(query, pageKey) }, + saver = { _, query, pageKey, list -> onSaveToStorage(query, pageKey, list) }, + externalQueryProvider = externalQueryProvider, + ).asPagedStore() + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedQueryBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedQueryBuilderImpl.kt index 7cd1e0d..9c0e296 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedQueryBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedQueryBuilderImpl.kt @@ -38,7 +38,7 @@ internal class PagedQueryBuilderImpl( override fun build(onFetch: suspend (Q, P) -> PagedList): PagedQueryStore { return PagedKeyedQueryStoreImpl( - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, fetcher = { _, query, pageKey -> onFetch(query, pageKey) }, @@ -47,7 +47,7 @@ internal class PagedQueryBuilderImpl( override fun buildCustom(loader: suspend PageEmitter.(Q, P) -> Unit): PagedQueryStore { return PagedKeyedQueryStoreImpl( - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, fetcher = CorePageFetcher.Custom { _, query, pageKey -> loader(query, pageKey) }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedQuerySuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedQuerySuspendingBuilderImpl.kt index abd7960..81a5429 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedQuerySuspendingBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedQuerySuspendingBuilderImpl.kt @@ -39,7 +39,7 @@ internal class PagedQuerySuspendingBuilderImpl( onLoadFromStorage: suspend (Q, P) -> PagedList? ): PagedQueryStore { return PagedKeyedQueryStoreImpl( - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, fetcher = { _, query, pageKey -> onFetch(query, pageKey) }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedSuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedSuspendingBuilderImpl.kt index 6215a29..6a13d68 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/paged/PagedSuspendingBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/paged/PagedSuspendingBuilderImpl.kt @@ -1,6 +1,7 @@ package com.elveum.store.internal.builders.paged import com.elveum.store.builders.BasePagedBuilder +import com.elveum.store.builders.PagedExternalQuerySuspendingBuilder import com.elveum.store.builders.PagedKeyedSuspendingBuilder import com.elveum.store.builders.PagedQuerySuspendingBuilder import com.elveum.store.builders.PagedSuspendingBuilder @@ -10,6 +11,8 @@ import com.elveum.store.internal.stores.PagedKeyedQueryStoreImpl import com.elveum.store.internal.stores.asPagedStore import com.elveum.store.stores.paged.PagedList import com.elveum.store.stores.paged.PagedStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow internal class PagedSuspendingBuilderImpl

( private val config: SharedPageConfig, @@ -24,6 +27,31 @@ internal class PagedSuspendingBuilderImpl

( return PagedQuerySuspendingBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: () -> Flow, + ): PagedExternalQuerySuspendingBuilder { + return PagedExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: () -> StateFlow, + ): PagedExternalQuerySuspendingBuilder { + return PagedExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { queryFlow().value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun withKeys(): PagedKeyedSuspendingBuilder { return PagedKeyedSuspendingBuilderImpl(config) } @@ -42,7 +70,7 @@ internal class PagedSuspendingBuilderImpl

( onLoadFromStorage: suspend (P) -> PagedList? ): PagedStore { return PagedKeyedQueryStoreImpl( - initialQuery = Unit, + initialQueryProvider = { Unit }, queryDebounceMillis = 0, config = config, fetcher = { _, _, pageKey -> onFetch(pageKey) }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleBuilderImpl.kt index 6d32684..d337793 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleBuilderImpl.kt @@ -2,6 +2,7 @@ package com.elveum.store.internal.builders.simple import com.elveum.container.Emitter import com.elveum.store.builders.SimpleBuilder +import com.elveum.store.builders.SimpleExternalQueryBuilder import com.elveum.store.builders.SimpleKeyedBuilder import com.elveum.store.builders.SimpleQueryBuilder import com.elveum.store.builders.SimpleReactiveBuilder @@ -15,6 +16,8 @@ import com.elveum.store.internal.stores.KeyedQueryStoreImpl import com.elveum.store.internal.stores.asSimpleStore import com.elveum.store.internal.stores.common.CoreFetcher import com.elveum.store.stores.simple.SimpleStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow internal class SimpleBuilderImpl( val sharedBuilder: BaseBuilderImpl> = BaseBuilderImpl(), @@ -28,6 +31,31 @@ internal class SimpleBuilderImpl( return SimpleQueryBuilderImpl(initialQuery, debounceMillis, sharedBuilder.config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: () -> Flow, + ): SimpleExternalQueryBuilder { + return SimpleExternalQueryBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = sharedBuilder.config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: () -> StateFlow, + ): SimpleExternalQueryBuilder { + return SimpleExternalQueryBuilderImpl( + initialQueryProvider = { queryFlow().value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = sharedBuilder.config, + ) + } + override fun withKeys(): SimpleKeyedBuilder { return SimpleKeyedBuilderImpl(sharedBuilder.config) } @@ -48,7 +76,7 @@ internal class SimpleBuilderImpl( return KeyedQueryStoreImpl( fetcher = { _, _ -> onFetch() }, config = sharedBuilder.config, - initialQuery = Unit, + initialQueryProvider = { Unit }, ).asSimpleStore() } @@ -56,7 +84,7 @@ internal class SimpleBuilderImpl( return KeyedQueryStoreImpl( fetcher = CoreFetcher.Custom { _, _ -> loader() }, config = sharedBuilder.config, - initialQuery = Unit, + initialQueryProvider = { Unit }, ).asSimpleStore() } diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryBuilderImpl.kt new file mode 100644 index 0000000..0328bae --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryBuilderImpl.kt @@ -0,0 +1,80 @@ +package com.elveum.store.internal.builders.simple + +import com.elveum.container.Emitter +import com.elveum.store.builders.SimpleExternalQueryBuilder +import com.elveum.store.builders.SimpleExternalQueryReactiveBuilder +import com.elveum.store.builders.SimpleExternalQueryReactiveNoFetcherBuilder +import com.elveum.store.builders.SimpleExternalQuerySuspendingBuilder +import com.elveum.store.builders.SimpleKeyedExternalQueryBuilder +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleQueryContract +import com.elveum.store.internal.builders.BaseBuilderImpl +import com.elveum.store.internal.builders.SharedConfig +import com.elveum.store.internal.builders.keyed.SimpleKeyedExternalQueryBuilderImpl +import com.elveum.store.internal.stores.KeyedQueryStoreImpl +import com.elveum.store.internal.stores.asSimpleStore +import com.elveum.store.internal.stores.common.CoreFetcher +import com.elveum.store.stores.simple.SimpleStore +import kotlinx.coroutines.flow.Flow + +internal class SimpleExternalQueryBuilderImpl( + private val initialQueryProvider: (Unit) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: () -> Flow, + private val config: SharedConfig, + sharedBuilder: BaseBuilderImpl> = BaseBuilderImpl(config), +) : SimpleExternalQueryBuilder, BaseBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + private val externalQueryProvider: (Unit) -> Flow = { queryFlow() } + + override fun addSuspendingLocalStorage(): SimpleExternalQuerySuspendingBuilder { + return SimpleExternalQuerySuspendingBuilderImpl(initialQueryProvider, queryDebounceMillis, queryFlow, config) + } + + override fun addReactiveLocalStorage(): SimpleExternalQueryReactiveBuilder { + return SimpleExternalQueryReactiveBuilderImpl(initialQueryProvider, queryDebounceMillis, queryFlow, config) + } + + override fun disableFetcher(): SimpleExternalQueryReactiveNoFetcherBuilder { + return SimpleExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider, queryDebounceMillis, queryFlow, config, + ) + } + + override fun withKeys(): SimpleKeyedExternalQueryBuilder { + return SimpleKeyedExternalQueryBuilderImpl( + initialQueryProvider = { initialQueryProvider(Unit) }, + queryDebounceMillis = queryDebounceMillis, + queryFlow = { queryFlow() }, + config = config, + ) + } + + override fun build(contract: SimpleQueryContract): SimpleStore { + return build(onFetch = contract::fetch) + } + + override fun build(onFetch: suspend (Q) -> T): SimpleStore { + return KeyedQueryStoreImpl( + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + config = config, + fetcher = { _, query -> onFetch(query) }, + externalQueryProvider = externalQueryProvider, + ).asSimpleStore() + } + + override fun buildCustom(loader: suspend Emitter.(Q) -> Unit): SimpleStore { + return KeyedQueryStoreImpl( + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + config = config, + fetcher = CoreFetcher.Custom { _, query -> loader(query) }, + externalQueryProvider = externalQueryProvider, + ).asSimpleStore() + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryReactiveBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryReactiveBuilderImpl.kt new file mode 100644 index 0000000..0c0307e --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryReactiveBuilderImpl.kt @@ -0,0 +1,69 @@ +package com.elveum.store.internal.builders.simple + +import com.elveum.store.builders.SimpleExternalQueryReactiveBuilder +import com.elveum.store.builders.SimpleExternalQueryReactiveNoFetcherBuilder +import com.elveum.store.builders.SimpleKeyedExternalQueryReactiveBuilder +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleQueryReactiveContract +import com.elveum.store.internal.builders.BaseBuilderImpl +import com.elveum.store.internal.builders.SharedConfig +import com.elveum.store.internal.builders.keyed.SimpleKeyedExternalQueryReactiveBuilderImpl +import com.elveum.store.internal.stores.KeyedQueryStoreImpl +import com.elveum.store.internal.stores.asSimpleStore +import com.elveum.store.stores.simple.SimpleStore +import kotlinx.coroutines.flow.Flow + +internal class SimpleExternalQueryReactiveBuilderImpl( + private val initialQueryProvider: (Unit) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: () -> Flow, + private val config: SharedConfig, + sharedBuilder: BaseBuilderImpl> = BaseBuilderImpl(config), +) : SimpleExternalQueryReactiveBuilder, + BaseBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + private val externalQueryProvider: (Unit) -> Flow = { queryFlow() } + + override fun disableFetcher(): SimpleExternalQueryReactiveNoFetcherBuilder { + return SimpleExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider, queryDebounceMillis, queryFlow, config, + ) + } + + override fun withKeys(): SimpleKeyedExternalQueryReactiveBuilder { + return SimpleKeyedExternalQueryReactiveBuilderImpl( + initialQueryProvider = { initialQueryProvider(Unit) }, + queryDebounceMillis = queryDebounceMillis, + queryFlow = { queryFlow() }, + config = config, + ) + } + + override fun build(contract: SimpleQueryReactiveContract): SimpleStore { + return build( + onFetch = contract::fetch, + onSaveToStorage = contract::saveToLocalStorage, + onObserveStorage = contract::observeLocalStorage, + ) + } + + override fun build( + onFetch: suspend (Q) -> T, + onSaveToStorage: suspend (Q, T) -> Unit, + onObserveStorage: (Q) -> Flow, + ): SimpleStore { + return KeyedQueryStoreImpl( + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + config = config, + fetcher = { _, query -> onFetch(query) }, + saver = { _, query, value -> onSaveToStorage(query, value) }, + observer = { _, query -> onObserveStorage(query) }, + externalQueryProvider = externalQueryProvider, + ).asSimpleStore() + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryReactiveNoFetcherBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryReactiveNoFetcherBuilderImpl.kt new file mode 100644 index 0000000..2e910cd --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQueryReactiveNoFetcherBuilderImpl.kt @@ -0,0 +1,55 @@ +package com.elveum.store.internal.builders.simple + +import com.elveum.store.builders.SimpleExternalQueryReactiveNoFetcherBuilder +import com.elveum.store.builders.SimpleKeyedExternalQueryReactiveNoFetcherBuilder +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleQueryReactiveNoFetcherContract +import com.elveum.store.internal.builders.BaseBuilderImpl +import com.elveum.store.internal.builders.SharedConfig +import com.elveum.store.internal.builders.keyed.SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl +import com.elveum.store.internal.stores.KeyedQueryStoreImpl +import com.elveum.store.internal.stores.asSimpleStore +import com.elveum.store.stores.simple.SimpleStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +internal class SimpleExternalQueryReactiveNoFetcherBuilderImpl( + private val initialQueryProvider: (Unit) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: () -> Flow, + private val config: SharedConfig, + sharedBuilder: BaseBuilderImpl> = BaseBuilderImpl(config), +) : SimpleExternalQueryReactiveNoFetcherBuilder, + BaseBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + private val externalQueryProvider: (Unit) -> Flow = { queryFlow() } + + override fun withKeys(): SimpleKeyedExternalQueryReactiveNoFetcherBuilder { + return SimpleKeyedExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider = { initialQueryProvider(Unit) }, + queryDebounceMillis = queryDebounceMillis, + queryFlow = { queryFlow() }, + config = config, + ) + } + + override fun build(contract: SimpleQueryReactiveNoFetcherContract): SimpleStore { + return build(onObserve = { contract.observe(it) }) + } + + override fun build(onObserve: (Q) -> Flow): SimpleStore { + return KeyedQueryStoreImpl( + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + config = config, + fetcher = { _, query -> onObserve(query).first() }, + saver = { _, _, _ -> }, + observer = { _, query -> onObserve(query) }, + externalQueryProvider = externalQueryProvider, + ).asSimpleStore() + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQuerySuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQuerySuspendingBuilderImpl.kt new file mode 100644 index 0000000..127b77d --- /dev/null +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleExternalQuerySuspendingBuilderImpl.kt @@ -0,0 +1,62 @@ +package com.elveum.store.internal.builders.simple + +import com.elveum.store.builders.SimpleExternalQuerySuspendingBuilder +import com.elveum.store.builders.SimpleKeyedExternalQuerySuspendingBuilder +import com.elveum.store.builders.base.BaseBuilder +import com.elveum.store.contracts.SimpleQuerySuspendingContract +import com.elveum.store.internal.builders.BaseBuilderImpl +import com.elveum.store.internal.builders.SharedConfig +import com.elveum.store.internal.builders.keyed.SimpleKeyedExternalQuerySuspendingBuilderImpl +import com.elveum.store.internal.stores.KeyedQueryStoreImpl +import com.elveum.store.internal.stores.asSimpleStore +import com.elveum.store.stores.simple.SimpleStore +import kotlinx.coroutines.flow.Flow + +internal class SimpleExternalQuerySuspendingBuilderImpl( + private val initialQueryProvider: (Unit) -> Q, + private val queryDebounceMillis: Long, + private val queryFlow: () -> Flow, + private val config: SharedConfig, + sharedBuilder: BaseBuilderImpl> = BaseBuilderImpl(config), +) : SimpleExternalQuerySuspendingBuilder, + BaseBuilder> by sharedBuilder { + + init { + sharedBuilder.setReference(this) + } + + private val externalQueryProvider: (Unit) -> Flow = { queryFlow() } + + override fun withKeys(): SimpleKeyedExternalQuerySuspendingBuilder { + return SimpleKeyedExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { initialQueryProvider(Unit) }, + queryDebounceMillis = queryDebounceMillis, + queryFlow = { queryFlow() }, + config = config, + ) + } + + override fun build(contract: SimpleQuerySuspendingContract): SimpleStore { + return build( + onFetch = contract::fetch, + onSaveToStorage = contract::saveToLocalStorage, + onLoadFromStorage = contract::loadFromLocalStorage, + ) + } + + override fun build( + onFetch: suspend (Q) -> T, + onSaveToStorage: suspend (Q, T) -> Unit, + onLoadFromStorage: suspend (Q) -> T?, + ): SimpleStore { + return KeyedQueryStoreImpl( + initialQueryProvider = initialQueryProvider, + queryDebounceMillis = queryDebounceMillis, + config = config, + fetcher = { _, query -> onFetch(query) }, + saver = { _, query, value -> onSaveToStorage(query, value) }, + loader = { _, query -> onLoadFromStorage(query) }, + externalQueryProvider = externalQueryProvider, + ).asSimpleStore() + } +} diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryBuilderImpl.kt index 0c9b5e8..fcb058e 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryBuilderImpl.kt @@ -50,7 +50,7 @@ internal class SimpleQueryBuilderImpl( override fun build(onFetch: suspend (Q) -> T): SimpleQueryStore { return KeyedQueryStoreImpl( - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, fetcher = { _, query -> onFetch(query) }, @@ -59,7 +59,7 @@ internal class SimpleQueryBuilderImpl( override fun buildCustom(loader: suspend Emitter.(Q) -> Unit): SimpleQueryStore { return KeyedQueryStoreImpl( - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, fetcher = CoreFetcher.Custom { _, query -> loader(query) }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryReactiveBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryReactiveBuilderImpl.kt index 6724031..6676565 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryReactiveBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryReactiveBuilderImpl.kt @@ -50,7 +50,7 @@ internal class SimpleQueryReactiveBuilderImpl( onObserveStorage: (Q) -> Flow ): SimpleQueryStore { return KeyedQueryStoreImpl( - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, fetcher = { _, query -> onFetch(query) }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryReactiveNoFetcherBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryReactiveNoFetcherBuilderImpl.kt index 4a5e197..caec970 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryReactiveNoFetcherBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQueryReactiveNoFetcherBuilderImpl.kt @@ -36,7 +36,7 @@ internal class SimpleQueryReactiveNoFetcherBuilderImpl( override fun build(onObserve: (Q) -> Flow): SimpleQueryStore { return KeyedQueryStoreImpl( - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, fetcher = { _, query -> onObserve(query).first() }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQuerySuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQuerySuspendingBuilderImpl.kt index 68761b3..50186d5 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQuerySuspendingBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleQuerySuspendingBuilderImpl.kt @@ -40,7 +40,7 @@ internal class SimpleQuerySuspendingBuilderImpl( onLoadFromStorage: suspend (Q) -> T? ): SimpleQueryStore { return KeyedQueryStoreImpl( - initialQuery = initialQuery, + initialQueryProvider = { initialQuery }, queryDebounceMillis = queryDebounceMillis, config = config, fetcher = { _, query -> onFetch(query) }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleReactiveBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleReactiveBuilderImpl.kt index 61ce8a7..e3bd049 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleReactiveBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleReactiveBuilderImpl.kt @@ -1,5 +1,6 @@ package com.elveum.store.internal.builders.simple +import com.elveum.store.builders.SimpleExternalQueryReactiveBuilder import com.elveum.store.builders.SimpleKeyedReactiveBuilder import com.elveum.store.builders.SimpleQueryReactiveBuilder import com.elveum.store.builders.SimpleReactiveBuilder @@ -13,6 +14,7 @@ import com.elveum.store.internal.stores.KeyedQueryStoreImpl import com.elveum.store.internal.stores.asSimpleStore import com.elveum.store.stores.simple.SimpleStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow internal class SimpleReactiveBuilderImpl( private val config: SharedConfig, @@ -31,6 +33,31 @@ internal class SimpleReactiveBuilderImpl( return SimpleQueryReactiveBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: () -> Flow, + ): SimpleExternalQueryReactiveBuilder { + return SimpleExternalQueryReactiveBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: () -> StateFlow, + ): SimpleExternalQueryReactiveBuilder { + return SimpleExternalQueryReactiveBuilderImpl( + initialQueryProvider = { queryFlow().value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun disableFetcher(): SimpleReactiveNoFetcherBuilder { return SimpleReactiveNoFetcherBuilderImpl(config) } @@ -41,7 +68,7 @@ internal class SimpleReactiveBuilderImpl( onObserveStorage: () -> Flow ): SimpleStore { return KeyedQueryStoreImpl( - initialQuery = Unit, + initialQueryProvider = { Unit }, config = config, fetcher = { _, _ -> onFetch() }, saver = { _, _, data -> onSaveToStorage(data) }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleReactiveNoFetcherBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleReactiveNoFetcherBuilderImpl.kt index 83970a3..8dd608a 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleReactiveNoFetcherBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleReactiveNoFetcherBuilderImpl.kt @@ -1,5 +1,6 @@ package com.elveum.store.internal.builders.simple +import com.elveum.store.builders.SimpleExternalQueryReactiveNoFetcherBuilder import com.elveum.store.builders.SimpleKeyedReactiveNoFetcherBuilder import com.elveum.store.builders.SimpleQueryReactiveNoFetcherBuilder import com.elveum.store.builders.SimpleReactiveNoFetcherBuilder @@ -12,6 +13,7 @@ import com.elveum.store.internal.stores.KeyedQueryStoreImpl import com.elveum.store.internal.stores.asSimpleStore import com.elveum.store.stores.simple.SimpleStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first internal class SimpleReactiveNoFetcherBuilderImpl( @@ -34,13 +36,38 @@ internal class SimpleReactiveNoFetcherBuilderImpl( return SimpleQueryReactiveNoFetcherBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: () -> Flow, + ): SimpleExternalQueryReactiveNoFetcherBuilder { + return SimpleExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: () -> StateFlow, + ): SimpleExternalQueryReactiveNoFetcherBuilder { + return SimpleExternalQueryReactiveNoFetcherBuilderImpl( + initialQueryProvider = { queryFlow().value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun build(contract: SimpleReactiveNoFetcherContract): SimpleStore { return build(onObserve = contract::observe) } override fun build(onObserve: () -> Flow): SimpleStore { return KeyedQueryStoreImpl( - initialQuery = Unit, + initialQueryProvider = { Unit }, fetcher = { _, _ -> onObserve().first() }, saver = { _, _, _ -> }, observer = { _, _ -> onObserve() }, diff --git a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleSuspendingBuilderImpl.kt b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleSuspendingBuilderImpl.kt index b33b039..4c96e32 100644 --- a/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleSuspendingBuilderImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/builders/simple/SimpleSuspendingBuilderImpl.kt @@ -1,5 +1,6 @@ package com.elveum.store.internal.builders.simple +import com.elveum.store.builders.SimpleExternalQuerySuspendingBuilder import com.elveum.store.builders.SimpleKeyedSuspendingBuilder import com.elveum.store.builders.SimpleQuerySuspendingBuilder import com.elveum.store.builders.SimpleSuspendingBuilder @@ -11,6 +12,8 @@ import com.elveum.store.internal.builders.keyed.SimpleKeyedSuspendingBuilderImpl import com.elveum.store.internal.stores.KeyedQueryStoreImpl import com.elveum.store.internal.stores.asSimpleStore import com.elveum.store.stores.simple.SimpleStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow internal class SimpleSuspendingBuilderImpl( private val config: SharedConfig, @@ -29,6 +32,31 @@ internal class SimpleSuspendingBuilderImpl( return SimpleQuerySuspendingBuilderImpl(initialQuery, debounceMillis, config) } + override fun withQuery( + initialQuery: Q, + debounceMillis: Long, + queryFlow: () -> Flow, + ): SimpleExternalQuerySuspendingBuilder { + return SimpleExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { initialQuery }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + + override fun withQuery( + debounceMillis: Long, + queryFlow: () -> StateFlow, + ): SimpleExternalQuerySuspendingBuilder { + return SimpleExternalQuerySuspendingBuilderImpl( + initialQueryProvider = { queryFlow().value }, + queryDebounceMillis = debounceMillis, + queryFlow = queryFlow, + config = config, + ) + } + override fun build(contract: SimpleSuspendingContract): SimpleStore { return build( onFetch = contract::fetch, @@ -43,7 +71,7 @@ internal class SimpleSuspendingBuilderImpl( onLoadFromStorage: suspend () -> T? ): SimpleStore { return KeyedQueryStoreImpl( - initialQuery = Unit, + initialQueryProvider = { Unit }, config = config, fetcher = { _, _ -> onFetch() }, saver = { _, _, data -> onSaveToStorage(data) }, diff --git a/store/src/main/java/com/elveum/store/internal/stores/KeyedQueryStoreImpl.kt b/store/src/main/java/com/elveum/store/internal/stores/KeyedQueryStoreImpl.kt index fe3aaf4..a09b501 100644 --- a/store/src/main/java/com/elveum/store/internal/stores/KeyedQueryStoreImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/stores/KeyedQueryStoreImpl.kt @@ -26,15 +26,17 @@ internal class KeyedQueryStoreImpl( private val saver: suspend (Key, Q, T) -> Unit = { _, _, _ -> }, private val observer: (Key, Q) -> Flow = { _, _ -> flowOf(null) }, private val config: SharedConfig, - private val initialQuery: Q, + private val initialQueryProvider: (Key) -> Q, private val queryDebounceMillis: Long = 0, + private val externalQueryProvider: ((Key) -> Flow)? = null, ) : KeyedQueryStore { private val coreStore = CoreStore( - initialQuery = initialQuery, + initialQueryProvider = initialQueryProvider, queryDebounceMillis = queryDebounceMillis, config = config, observer = observer, + externalQueryProvider = externalQueryProvider, valueLoaderProvider = object : CoreValueLoaderProvider { override fun provideValueLoader( key: Key, @@ -70,11 +72,12 @@ internal class KeyedQueryStoreImpl( saver: suspend (Key, Q, T) -> Unit = { _, _, _ -> }, observer: (Key, Q) -> Flow = { _, _ -> flowOf(null) }, config: SharedConfig, - initialQuery: Q, + initialQueryProvider: (Key) -> Q, queryDebounceMillis: Long = 0, + externalQueryProvider: ((Key) -> Flow)? = null, ) : this( fetcher = CoreFetcher.Default(fetcher), - loader, saver, observer, config, initialQuery, queryDebounceMillis, + loader, saver, observer, config, initialQueryProvider, queryDebounceMillis, externalQueryProvider, ) override fun observeQueryFlow(key: Key) = coreStore.observeQueryFlow(key) diff --git a/store/src/main/java/com/elveum/store/internal/stores/PagedKeyedQueryStoreImpl.kt b/store/src/main/java/com/elveum/store/internal/stores/PagedKeyedQueryStoreImpl.kt index 8b8d4cc..f508578 100644 --- a/store/src/main/java/com/elveum/store/internal/stores/PagedKeyedQueryStoreImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/stores/PagedKeyedQueryStoreImpl.kt @@ -28,15 +28,17 @@ internal class PagedKeyedQueryStoreImpl PagedList? = { _, _, _ -> null }, private val saver: suspend (Key, Q, PageKey, PagedList) -> Unit = { _, _, _, _ -> }, private val config: SharedPageConfig, - private val initialQuery: Q, + private val initialQueryProvider: (Key) -> Q, private val queryDebounceMillis: Long = 0, + private val externalQueryProvider: ((Key) -> Flow)? = null, ) : PagedKeyedQueryStore { private val coreStore = CoreStore, PagedList>( - initialQuery = initialQuery, + initialQueryProvider = initialQueryProvider, queryDebounceMillis = queryDebounceMillis, config = config, observer = { _, _ -> flowOf(null) }, + externalQueryProvider = externalQueryProvider, valueLoaderProvider = object : CoreValueLoaderProvider, PagedList> { override fun provideValueLoader( key: Key, @@ -76,11 +78,12 @@ internal class PagedKeyedQueryStoreImpl PagedList? = { _, _, _ -> null }, saver: suspend (Key, Q, PageKey, PagedList) -> Unit = { _, _, _, _ -> }, config: SharedPageConfig, - initialQuery: Q, + initialQueryProvider: (Key) -> Q, queryDebounceMillis: Long = 0, + externalQueryProvider: ((Key) -> Flow)? = null, ) : this( fetcher = CorePageFetcher.Default(fetcher), - loader, saver, config, initialQuery, queryDebounceMillis, + loader, saver, config, initialQueryProvider, queryDebounceMillis, externalQueryProvider, ) override fun observeQueryFlow(key: Key) = coreStore.observeQueryFlow(key) diff --git a/store/src/main/java/com/elveum/store/internal/stores/PagedStoreImpl.kt b/store/src/main/java/com/elveum/store/internal/stores/PagedStoreImpl.kt index 3d50f0f..d80f39b 100644 --- a/store/src/main/java/com/elveum/store/internal/stores/PagedStoreImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/stores/PagedStoreImpl.kt @@ -6,8 +6,8 @@ import com.elveum.store.stores.base.OptimisticUpdateScope import com.elveum.store.stores.paged.PagedStore import kotlinx.coroutines.flow.Flow -internal class PagedStoreImpl

( - private val origin: PagedKeyedQueryStoreImpl, +internal class PagedStoreImpl( + private val origin: PagedKeyedQueryStoreImpl, ) : PagedStore { override fun onItemRendered(index: Int) = origin.onItemRendered(Unit, index) @@ -36,5 +36,5 @@ internal class PagedStoreImpl

( } } -internal fun

PagedKeyedQueryStoreImpl.asPagedStore() = +internal fun PagedKeyedQueryStoreImpl.asPagedStore() = PagedStoreImpl(this) diff --git a/store/src/main/java/com/elveum/store/internal/stores/SimpleStoreImpl.kt b/store/src/main/java/com/elveum/store/internal/stores/SimpleStoreImpl.kt index 5459ca3..71a52bf 100644 --- a/store/src/main/java/com/elveum/store/internal/stores/SimpleStoreImpl.kt +++ b/store/src/main/java/com/elveum/store/internal/stores/SimpleStoreImpl.kt @@ -6,8 +6,8 @@ import com.elveum.store.stores.base.OptimisticUpdateScope import com.elveum.store.stores.simple.SimpleStore import kotlinx.coroutines.flow.Flow -internal class SimpleStoreImpl( - private val origin: KeyedQueryStoreImpl, +internal class SimpleStoreImpl( + private val origin: KeyedQueryStoreImpl, ) : SimpleStore { override fun observe(request: LoadRequest?): Flow> { return origin.observe(Unit, request) @@ -39,5 +39,5 @@ internal class SimpleStoreImpl( } } -internal fun KeyedQueryStoreImpl.asSimpleStore() = +internal fun KeyedQueryStoreImpl.asSimpleStore() = SimpleStoreImpl(this) diff --git a/store/src/main/java/com/elveum/store/internal/stores/common/CoreStore.kt b/store/src/main/java/com/elveum/store/internal/stores/common/CoreStore.kt index 19cbb4e..872448f 100644 --- a/store/src/main/java/com/elveum/store/internal/stores/common/CoreStore.kt +++ b/store/src/main/java/com/elveum/store/internal/stores/common/CoreStore.kt @@ -52,11 +52,12 @@ import kotlin.collections.map @Suppress("TooManyFunctions") internal class CoreStore( - private val initialQuery: Q, + private val initialQueryProvider: (Key) -> Q, private val queryDebounceMillis: Long, private val config: SharedConfig, private val observer: (Key, Q) -> Flow, private val valueLoaderProvider: CoreValueLoaderProvider, + private val externalQueryProvider: ((Key) -> Flow)? = null, ) { private val gates = ConcurrentHashMap() @@ -124,23 +125,35 @@ internal class CoreStore( processItems( flow = keysFlow, onAdded = { key -> - currentQueriesFlow.update { oldMap -> oldMap + (key to initialQuery) } - asyncRequests[key] = MutableStateFlow(initialQuery) + currentQueriesFlow.update { oldMap -> oldMap + (key to initialQueryProvider(key)) } + asyncRequests[key] = MutableStateFlow(initialQueryProvider(key)) }, onRemoved = { key -> currentQueriesFlow.update { oldMap -> oldMap - key } asyncRequests.remove(key) } ) { key -> - asyncRequests[key] - // drop the StateFlow's initial replayed value, otherwise the key would be - // reloaded immediately on activation - right after observe() already started - // the initial load (mirrors observeLocalChanges below). - ?.drop(1) - ?.debounce(queryDebounceMillis) - ?.collect { - invalidateAsync(key) + coroutineScope { + externalQueryProvider?.let { provider -> + launch { + // Feed each external emission into the same async-query pipeline used by + // submitQueryAsync. asyncRequests[key] is a StateFlow, so an emission equal + // to the current query is de-duplicated and does not trigger a reload - + // this prevents a double initial load when the flow's first value equals + // the seed. + provider(key).collect { query -> submitQueryAsync(key, query) } + } } + asyncRequests[key] + // drop the StateFlow's initial replayed value, otherwise the key would be + // reloaded immediately on activation - right after observe() already started + // the initial load (mirrors observeLocalChanges below). + ?.drop(1) + ?.debounce(queryDebounceMillis) + ?.collect { + invalidateAsync(key) + } + } awaitCancellation() } } @@ -239,7 +252,7 @@ internal class CoreStore( fun observeQueryFlow(key: Key): StateFlow { - return currentQueriesFlow.stateMap { it[key] ?: initialQuery } + return currentQueriesFlow.stateMap { it[key] ?: initialQueryProvider(key) } } private fun createDelegate(): CoreLoaderDelegate { diff --git a/store/src/test/java/com/elveum/store/keyed/KeyedExternalQueryStoreTest.kt b/store/src/test/java/com/elveum/store/keyed/KeyedExternalQueryStoreTest.kt new file mode 100644 index 0000000..99cdc49 --- /dev/null +++ b/store/src/test/java/com/elveum/store/keyed/KeyedExternalQueryStoreTest.kt @@ -0,0 +1,100 @@ +package com.elveum.store.keyed + +import com.elveum.store.load.StoreResult +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import org.junit.Assert.assertEquals +import org.junit.Test + +class KeyedExternalQueryStoreTest : AbstractKeyedStoreTest() { + + @Test + fun `GIVEN keyed external query WHEN each key observed THEN each key follows its own query flow`() = runFlowTest { + val queries = mapOf("k1" to MutableStateFlow("a"), "k2" to MutableStateFlow("b")) + val store = storeBuilder() + .withQuery { key -> queries.getValue(key) } + .build { key, query -> "$key-$query" } + + val c1 = store.observe("k1").startCollecting() + val c2 = store.observe("k2").startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded("k1-a"), c1.lastItem) + assertResult(StoreResult.Loaded("k2-b"), c2.lastItem) + + queries.getValue("k1").value = "a2" + runCurrent() + + assertResult(StoreResult.Loaded("k1-a2"), c1.lastItem) + assertResult(StoreResult.Loaded("k2-b"), c2.lastItem) // key 2 unaffected + } + + @Test + fun `GIVEN keyed external query primary overload WHEN observed THEN first load uses initial query`() = runFlowTest { + val store = storeBuilder() + .withQuery(initialQuery = "seed") { _ -> MutableStateFlow("seed") } + .build { key, query -> "$key-$query" } + + val c1 = store.observe("k1").startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded("k1-seed"), c1.lastItem) + } + + @Test + fun `GIVEN keyed external query suspending store WHEN flow emits THEN reloads and saves per key`() = runFlowTest { + val query = MutableStateFlow("q1") + val saved = mutableListOf>() + val store = storeBuilder() + .addSuspendingLocalStorage() + .withQuery { query } + .build( + onFetch = { key, q -> "$key-$q" }, + onSaveToStorage = { key, q, v -> saved += Triple(key, q, v) }, + onLoadFromStorage = { _, _ -> null }, + ) + val collector = store.observe("k1").startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("k1-q2"), collector.lastItem) + assertEquals(listOf(Triple("k1", "q1", "k1-q1"), Triple("k1", "q2", "k1-q2")), saved) + } + + @Test + fun `GIVEN keyed external query no-fetcher store WHEN flow emits THEN observes the new query's local flow`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .disableFetcher() + .withQuery { query } + .build(onObserve = { key, q -> flowOf("$key-$q") }) + val collector = store.observe("k1").startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("k1-q2"), collector.lastItem) + } + + @Test + fun `GIVEN keyed withQuery BEFORE addSuspendingLocalStorage WHEN built THEN store works and saves per key`() = runFlowTest { + val query = MutableStateFlow("q1") + val saved = mutableListOf>() + val store = storeBuilder() + .withQuery { query } + .addSuspendingLocalStorage() + .build( + onFetch = { key, q -> "$key-$q" }, + onSaveToStorage = { key, q, v -> saved += Triple(key, q, v) }, + onLoadFromStorage = { _, _ -> null }, + ) + val collector = store.observe("k1").startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("k1-q2"), collector.lastItem) + assertEquals(Triple("k1", "q2", "k1-q2"), saved.last()) + } +} diff --git a/store/src/test/java/com/elveum/store/keyed/PagedKeyedExternalQueryStoreTest.kt b/store/src/test/java/com/elveum/store/keyed/PagedKeyedExternalQueryStoreTest.kt new file mode 100644 index 0000000..a456269 --- /dev/null +++ b/store/src/test/java/com/elveum/store/keyed/PagedKeyedExternalQueryStoreTest.kt @@ -0,0 +1,103 @@ +package com.elveum.store.keyed + +import com.elveum.store.StoreFactory +import com.elveum.store.base.AbstractStoreTest +import com.elveum.store.load.StoreResult +import com.elveum.store.stores.keyed.PagedKeyedStore +import com.elveum.store.stores.paged.PagedList +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertEquals +import org.junit.Test + +class PagedKeyedExternalQueryStoreTest : AbstractStoreTest() { + + private fun store(queries: Map>): PagedKeyedStore = StoreFactory + .pagedStoreBuilder(initialKey = 0, itemId = { it }) + .setCoroutineScopeFactory(createStoreScopeFactory()) + .setFetchDistance(1) + .withKeys() + .withQuery { key -> queries.getValue(key) } + .build { key, query, pageKey -> keyedQueryPage(key, query, pageKey, totalPages = 2) } + + private fun keyedQueryPage( + key: String, + query: String, + pageKey: Int, + totalPages: Int, + ): PagedList { + val items = listOf( + "$key-$query-item${pageKey * 2}", + "$key-$query-item${pageKey * 2 + 1}", + ) + val nextKey = (pageKey + 1).takeIf { it < totalPages } + return PagedList(items, nextKey) + } + + @Test + fun `GIVEN paged keyed external query WHEN each key observed THEN each key follows its own query flow`() = runFlowTest { + val queries = mapOf("k1" to MutableStateFlow("a"), "k2" to MutableStateFlow("b")) + val store = store(queries) + + val c1 = store.observe("k1").startCollecting() + val c2 = store.observe("k2").startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded(listOf("k1-a-item0", "k1-a-item1")), c1.lastItem) + assertResult(StoreResult.Loaded(listOf("k2-b-item0", "k2-b-item1")), c2.lastItem) + + queries.getValue("k1").value = "a2" + runCurrent() + + assertResult(StoreResult.Loaded(listOf("k1-a2-item0", "k1-a2-item1")), c1.lastItem) + assertResult(StoreResult.Loaded(listOf("k2-b-item0", "k2-b-item1")), c2.lastItem) // k2 unaffected + } + + @Test + fun `GIVEN paginated key WHEN its query flow emits THEN only that key's pagination resets`() = runFlowTest { + val queries = mapOf("k1" to MutableStateFlow("q1"), "k2" to MutableStateFlow("q1")) + val store = store(queries) + val c1 = store.observe("k1").startCollecting() + val c2 = store.observe("k2").startCollecting() + runCurrent() + + // advance k1 to its second page + store.onItemRendered("k1", 1) + runCurrent() + assertResult( + StoreResult.Loaded(listOf("k1-q1-item0", "k1-q1-item1", "k1-q1-item2", "k1-q1-item3")), + c1.lastItem, + ) + + queries.getValue("k1").value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded(listOf("k1-q2-item0", "k1-q2-item1")), c1.lastItem) + // the other key is untouched: same query, same accumulated pages + assertResult(StoreResult.Loaded(listOf("k2-q1-item0", "k2-q1-item1")), c2.lastItem) + } + + @Test + fun `GIVEN paged keyed external query suspending store WHEN flow emits THEN reloads and saves the first page per key`() = runFlowTest { + val query = MutableStateFlow("q1") + val saved = mutableListOf>() + val store = StoreFactory + .pagedStoreBuilder(initialKey = 0, itemId = { it }) + .setCoroutineScopeFactory(createStoreScopeFactory()) + .setFetchDistance(1) + .withKeys() + .addSuspendingLocalStorage() + .withQuery { query } + .build( + onFetch = { key, q, pageKey -> keyedQueryPage(key, q, pageKey, totalPages = 2) }, + onSaveToStorage = { key, q, pageKey, _ -> saved += Triple(key, q, pageKey) }, + onLoadFromStorage = { _, _, _ -> null }, + ) + val collector = store.observe("k1").startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded(listOf("k1-q2-item0", "k1-q2-item1")), collector.lastItem) + assertEquals(Triple("k1", "q2", 0), saved.last()) + } +} diff --git a/store/src/test/java/com/elveum/store/paged/PagedExternalQueryStoreTest.kt b/store/src/test/java/com/elveum/store/paged/PagedExternalQueryStoreTest.kt new file mode 100644 index 0000000..f217c51 --- /dev/null +++ b/store/src/test/java/com/elveum/store/paged/PagedExternalQueryStoreTest.kt @@ -0,0 +1,155 @@ +package com.elveum.store.paged + +import com.elveum.store.load.StoreResult +import com.elveum.store.stores.paged.PagedList +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertEquals +import org.junit.Test + +class PagedExternalQueryStoreTest : AbstractPagedStoreTest() { + + private fun queryPage(query: String, pageKey: Int, totalPages: Int): PagedList { + val items = listOf("$query-item${pageKey * 2}", "$query-item${pageKey * 2 + 1}") + val nextKey = (pageKey + 1).takeIf { it < totalPages } + return PagedList(items, nextKey) + } + + @Test + fun `GIVEN paged external StateFlow query WHEN observed THEN first load uses the flow's value`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .withQuery { query } + .build { q, pageKey -> queryPage(q, pageKey, totalPages = 2) } + + val collector = store.observe().startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded(listOf("q1-item0", "q1-item1")), collector.lastItem) + } + + @Test + fun `GIVEN two loaded pages WHEN flow emits new query THEN pagination resets and reloads first page`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .withQuery { query } + .build { q, pageKey -> queryPage(q, pageKey, totalPages = 2) } + val collector = store.observe().startCollecting() + runCurrent() + store.onItemRendered(1) // load the second page + runCurrent() + assertResult( + StoreResult.Loaded(listOf("q1-item0", "q1-item1", "q1-item2", "q1-item3")), + collector.lastItem, + ) + + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded(listOf("q2-item0", "q2-item1")), collector.lastItem) + } + + @Test + fun `GIVEN external StateFlow whose value equals seed WHEN observed THEN fetcher runs once for the first page`() = runFlowTest { + val query = MutableStateFlow("q1") + val fetchedQueries = mutableListOf() + val store = storeBuilder() + .withQuery { query } + .build { q, pageKey -> + fetchedQueries += q + queryPage(q, pageKey, totalPages = 2) + } + + store.observe().startCollecting() + runCurrent() + + assertEquals(listOf("q1"), fetchedQueries) // no double initial load + } + + @Test + fun `GIVEN paged external query with debounce WHEN flow emits quickly THEN only the last query is fetched`() = runFlowTest { + val query = MutableStateFlow("initial") + val fetchedQueries = mutableListOf() + val store = storeBuilder() + .withQuery(debounceMillis = 100) { query } + .build { q, pageKey -> + fetchedQueries += q + queryPage(q, pageKey, totalPages = 2) + } + val collector = store.observe().startCollecting() + runCurrent() + + query.value = "a" + advanceTimeBy(50) + query.value = "ab" + advanceTimeBy(101) + + assertResult(StoreResult.Loaded(listOf("ab-item0", "ab-item1")), collector.lastItem) + assertEquals(listOf("initial", "ab"), fetchedQueries) + } + + @Test + fun `GIVEN paged external query suspending store WHEN flow emits THEN reloads and saves the first page`() = runFlowTest { + val query = MutableStateFlow("q1") + val saved = mutableListOf>() + val store = storeBuilder() + .addSuspendingLocalStorage() + .withQuery { query } + .build( + onFetch = { q, pageKey -> queryPage(q, pageKey, totalPages = 2) }, + onSaveToStorage = { q, pageKey, _ -> saved += q to pageKey }, + onLoadFromStorage = { _, _ -> null }, + ) + val collector = store.observe().startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded(listOf("q2-item0", "q2-item1")), collector.lastItem) + assertEquals("q2" to 0, saved.last()) + } + + @Test + fun `GIVEN paged withQuery BEFORE addSuspendingLocalStorage WHEN built THEN store works`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .withQuery { query } + .addSuspendingLocalStorage() + .build( + onFetch = { q, pageKey -> queryPage(q, pageKey, totalPages = 2) }, + onSaveToStorage = { _, _, _ -> }, + onLoadFromStorage = { _, _ -> null }, + ) + val collector = store.observe().startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded(listOf("q2-item0", "q2-item1")), collector.lastItem) + } + + @Test + fun `GIVEN paged withQuery BEFORE withKeys WHEN built THEN keys share the query flow`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .withQuery { query } + .withKeys() + .build { key, q, pageKey -> + PagedList( + listOf("$key-$q-i${pageKey * 2}", "$key-$q-i${pageKey * 2 + 1}"), + (pageKey + 1).takeIf { it < 2 }, + ) + } + val c1 = store.observe("k1").startCollecting() + val c2 = store.observe("k2").startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded(listOf("k1-q1-i0", "k1-q1-i1")), c1.lastItem) + assertResult(StoreResult.Loaded(listOf("k2-q1-i0", "k2-q1-i1")), c2.lastItem) + + query.value = "q2" // shared flow: every key resets and reloads + runCurrent() + + assertResult(StoreResult.Loaded(listOf("k1-q2-i0", "k1-q2-i1")), c1.lastItem) + assertResult(StoreResult.Loaded(listOf("k2-q2-i0", "k2-q2-i1")), c2.lastItem) + } +} diff --git a/store/src/test/java/com/elveum/store/simple/SimpleExternalQueryStoreTest.kt b/store/src/test/java/com/elveum/store/simple/SimpleExternalQueryStoreTest.kt new file mode 100644 index 0000000..145f64d --- /dev/null +++ b/store/src/test/java/com/elveum/store/simple/SimpleExternalQueryStoreTest.kt @@ -0,0 +1,274 @@ +package com.elveum.store.simple + +import com.elveum.store.load.StoreResult +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.onSubscription +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.time.Duration.Companion.seconds + +class SimpleExternalQueryStoreTest : AbstractSimpleStoreTest() { + + @Test + fun `GIVEN imperative query store WHEN observed THEN loads with initial query`() = runFlowTest { + val store = storeBuilder() + .withQuery(initialQuery = "q1") + .build { query -> "value-$query" } + + val collector = store.observe().startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded("value-q1"), collector.lastItem) + assertEquals("q1", store.queryFlow.value) + } + + @Test + fun `GIVEN external StateFlow query WHEN observed THEN loads with the flow's current value`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .withQuery { query } + .build { q -> "value-$q" } + + val collector = store.observe().startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded("value-q1"), collector.lastItem) + } + + @Test + fun `GIVEN observed external-query store WHEN flow emits new query THEN data is re-fetched`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .withQuery { query } + .build { q -> "value-$q" } + val collector = store.observe().startCollecting() + runCurrent() + + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("value-q2"), collector.lastItem) + } + + @Test + fun `GIVEN external StateFlow whose value equals seed WHEN observed THEN fetcher runs exactly once`() = runFlowTest { + val query = MutableStateFlow("q1") + val fetchedQueries = mutableListOf() + val store = storeBuilder() + .withQuery { query } + .build { q -> fetchedQueries += q; "value-$q" } + + store.observe().startCollecting() + runCurrent() + + assertEquals(listOf("q1"), fetchedQueries) // no double initial load + } + + @Test + fun `GIVEN primary overload with explicit initial query WHEN flow later emits THEN first load uses initial then reloads`() = runFlowTest { + val query = MutableSharedFlow() // cold-ish: no initial value + val fetchedQueries = mutableListOf() + val store = storeBuilder() + .withQuery(initialQuery = "seed") { query } + .build { q -> fetchedQueries += q; "value-$q" } + val collector = store.observe().startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded("value-seed"), collector.lastItem) + + query.emit("q2") + runCurrent() + + assertResult(StoreResult.Loaded("value-q2"), collector.lastItem) + assertEquals(listOf("seed", "q2"), fetchedQueries) + } + + @Test + fun `GIVEN external query with debounce WHEN flow emits quickly THEN only last query is fetched`() = runFlowTest { + val query = MutableStateFlow("initial") + val fetchedQueries = mutableListOf() + val store = storeBuilder() + .withQuery(debounceMillis = 100) { query } + .build { q -> fetchedQueries += q; "value-$q" } + val collector = store.observe().startCollecting() + runCurrent() + + query.value = "a" + advanceTimeBy(50) + query.value = "ab" + advanceTimeBy(101) + + assertResult(StoreResult.Loaded("value-ab"), collector.lastItem) + assertEquals(listOf("initial", "ab"), fetchedQueries) + } + + @Test + fun `GIVEN external-query suspending store WHEN flow emits THEN reloads via remote and saves to storage`() = runFlowTest { + val query = MutableStateFlow("q1") + val saved = mutableListOf>() + val store = storeBuilder() + .addSuspendingLocalStorage() + .withQuery { query } + .build( + onFetch = { q -> "value-$q" }, + onSaveToStorage = { q, v -> saved += q to v }, + onLoadFromStorage = { null }, + ) + val collector = store.observe().startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("value-q2"), collector.lastItem) + assertEquals(listOf("q1" to "value-q1", "q2" to "value-q2"), saved) + } + + @Test + fun `GIVEN external-query reactive store WHEN flow emits THEN reloads and observes storage for the new query`() = runFlowTest { + val query = MutableStateFlow("q1") + val storages = mapOf( + "q1" to MutableStateFlow(null), + "q2" to MutableStateFlow(null), + ) + val store = storeBuilder() + .addReactiveLocalStorage() + .withQuery { query } + .build( + onFetch = { q -> "remote-$q" }, + onSaveToStorage = { q, v -> storages.getValue(q).value = v }, + onObserveStorage = { q -> storages.getValue(q) }, + ) + val collector = store.observe().startCollecting() + runCurrent() + assertResult(StoreResult.Loaded("remote-q1"), collector.lastItem) + + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("remote-q2"), collector.lastItem) + // a later reactive write to the q2 storage propagates automatically + storages.getValue("q2").value = "external-q2" + runCurrent() + assertResult(StoreResult.Loaded("external-q2"), collector.lastItem) + } + + @Test + fun `GIVEN external-query no-fetcher store WHEN flow emits THEN observes the new query's local flow`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .disableFetcher() + .withQuery { query } + .build(onObserve = { q -> flowOf("local-$q") }) + val collector = store.observe().startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("local-q2"), collector.lastItem) + } + + @Test + fun `GIVEN withQuery BEFORE addSuspendingLocalStorage WHEN built THEN store works and saves per query`() = runFlowTest { + val query = MutableStateFlow("q1") + val saved = mutableListOf>() + val store = storeBuilder() + .withQuery { query } + .addSuspendingLocalStorage() + .build( + onFetch = { q -> "value-$q" }, + onSaveToStorage = { q, v -> saved += q to v }, + onLoadFromStorage = { null }, + ) + val collector = store.observe().startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("value-q2"), collector.lastItem) + assertEquals(listOf("q1" to "value-q1", "q2" to "value-q2"), saved) + } + + @Test + fun `GIVEN withQuery BEFORE addReactiveLocalStorage WHEN built THEN store works`() = runFlowTest { + val query = MutableStateFlow("q1") + val storages = mapOf("q1" to MutableStateFlow(null), "q2" to MutableStateFlow(null)) + val store = storeBuilder() + .withQuery { query } + .addReactiveLocalStorage() + .build( + onFetch = { q -> "remote-$q" }, + onSaveToStorage = { q, v -> storages.getValue(q).value = v }, + onObserveStorage = { q -> storages.getValue(q) }, + ) + val collector = store.observe().startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("remote-q2"), collector.lastItem) + } + + @Test + fun `GIVEN withQuery BEFORE disableFetcher WHEN built THEN store observes local data`() = runFlowTest { + val query = MutableStateFlow("q1") + val store = storeBuilder() + .withQuery { query } + .disableFetcher() + .build(onObserve = { q -> flowOf("local-$q") }) + val collector = store.observe().startCollecting() + runCurrent() + query.value = "q2" + runCurrent() + + assertResult(StoreResult.Loaded("local-q2"), collector.lastItem) + } + + @Test + fun `GIVEN withQuery BEFORE withKeys WHEN built THEN all keys share the same query flow`() = runFlowTest { + val query = MutableStateFlow("a") + val store = storeBuilder() + .withQuery { query } + .withKeys() + .build { key, q -> "$key-$q" } + val c1 = store.observe("k1").startCollecting() + val c2 = store.observe("k2").startCollecting() + runCurrent() + + assertResult(StoreResult.Loaded("k1-a"), c1.lastItem) + assertResult(StoreResult.Loaded("k2-a"), c2.lastItem) + + query.value = "b" // shared flow: every key reloads + runCurrent() + + assertResult(StoreResult.Loaded("k1-b"), c1.lastItem) + assertResult(StoreResult.Loaded("k2-b"), c2.lastItem) + } + + @Test + fun `GIVEN external query store WHEN inactive THEN query flow is collected only while active`() = runFlowTest { + var subscriptions = 0 + val query = MutableStateFlow("q1") + val trackedFlow = query.onSubscription { subscriptions++ } + val store = storeBuilder() + .setInMemoryCacheTimeout(1.seconds) + .withQuery(initialQuery = "q1") { trackedFlow } + .build { q -> "value-$q" } + + assertEquals(0, subscriptions) // nothing collected before observing + + val collector = store.observe().startCollecting() + runCurrent() + assertEquals(1, subscriptions) // collected once active + + collector.cancel() + advanceTimeBy(1001) // pass the in-memory cache timeout -> store becomes inactive + runCurrent() + + query.value = "q2" // an emission while inactive must not trigger any work + runCurrent() + + assertEquals(1, subscriptions) // still exactly one subscription; no leak, no re-collect + } +}