Improve connectivity
Lint / lint (push) Failing after 18s

This commit is contained in:
Julien Valverdé committed 2026-10-11 22:17:16 +02:00
1 parent 0b636e0eaf
commit 43bc165a1c
13 files changed
+274 -45

No files matched your search

+4 -2
View File
@@ -234,13 +234,15 @@ actually a reactive read.
## Network mode
Mutations take a `networkMode` option, just like queries. When it is not set,
the `QueryClient`'s `defaultNetworkMode` is used, which is `"online"` unless
you change it. The connection status comes from the `AppConnectivity` service,
the `QueryClient`'s `defaultMutationNetworkMode` is used, which is `"online"`
unless you change it. It is separate from `defaultNetworkMode`, which only
applies to queries. The connection status comes from the `AppConnectivity` service,
as described in [Query](./query.md#network-mode-and-offline-support).
| `networkMode` | Behavior |
| --- | --- |
| `"online"` | A call made offline waits for a connection before running, and `state.paused` is `true` meanwhile. |
| `"offlineFirst"` | A call runs right away. If it fails while offline, it is `paused` until the connection returns, then runs again. |
| `"always"` | Calls run regardless of the connection. |
```ts
+8 -5
View File
@@ -22,7 +22,7 @@ If you already know TanStack Query, the main concepts translate directly:
| `fetchStatus: 'paused'` | `state.paused` |
| `refetch` | `query.refresh` or `query.refreshStream` |
| `invalidateQueries` | `query.invalidateCache` or `invalidateCacheEntry` |
| `networkMode` | `networkMode` (`"online"` or `"always"`) |
| `networkMode` | `networkMode` (`"online"`, `"offlineFirst"` or `"always"`) |
| `onlineManager` | The `AppConnectivity` service |
| `focusManager` | The `AppFocus` service |
@@ -315,7 +315,8 @@ const query = yield* Query.make({
```
Regaining focus resolves the current key again, subject to the same freshness
check. Focus comes from the `AppFocus` service: with
check. Set `refreshOnFocus: "always"` to refresh even when the result is still
fresh. Focus comes from the `AppFocus` service: with
`QueryClient.layerBrowser` it follows the window's `focus` and `blur` events.
Other platforms, such as React Native, provide it themselves, as shown in
[React Native](#react-native).
@@ -327,6 +328,7 @@ A query's `networkMode` decides how it behaves without a network connection:
| `networkMode` | Behavior |
| --- | --- |
| `"online"` (default) | A request waits for a connection before running `f`. Meanwhile its state is `paused`, with `result.waiting` still `true`. |
| `"offlineFirst"` | A request runs right away, e.g. to be served by a service worker or HTTP cache. If it fails while offline, it is `paused` until the connection returns, then runs again. |
| `"always"` | Requests ignore the connection. Suited to queries that do not use the network. |
Use `state.paused` to tell "loading" apart from "waiting for a connection":
@@ -337,9 +339,10 @@ const [state] = yield* View.useAll([query.state])
if (state.paused) return <p>Offline. Waiting for a connection…</p>
```
With `refreshOnReconnect` (enabled by default in `"online"` mode), regaining
the connection resolves the current key again, subject to the usual freshness
check. Like TanStack Query, focus and reconnect refreshes never restart a
With `refreshOnReconnect` (enabled by default, except in `"always"` mode),
regaining the connection resolves the current key again, subject to the usual
freshness check. Set it to `"always"` to refresh even when the result is still
fresh. Like TanStack Query, focus and reconnect refreshes never restart a
request that is already in flight: a running request keeps running, and one
paused for lack of a connection resumes on its own. A paused request can still
be superseded or interrupted like any other.
+4 -3
View File
@@ -59,14 +59,15 @@
- [`2ed417a`](https://git.valverde.cloud/Thilawyn/effect-view/commit/2ed417a49f283c120af4034d1a496d3fe304dd05) - Add offline support to `Query`, like TanStack Query's `networkMode`.
- The new `networkMode` option (`Operation.NetworkMode`) is `"online"` by default: a request made offline waits for a connection before running, and its state has `paused: true` meanwhile, with `result.waiting` still `true`. With `"always"`, requests ignore the connection.
- The new `refreshOnReconnect` option refreshes the current key when the connection returns, subject to `staleTime`. It defaults to `true` in `"online"` mode, and to `false` otherwise.
- The new `networkMode` option (`Operation.NetworkMode`) is `"online"` by default: a request made offline waits for a connection before running, and its state has `paused: true` meanwhile, with `result.waiting` still `true`. With `"offlineFirst"`, a request runs right away, and if it fails while offline, it is paused until the connection returns, then runs again. With `"always"`, requests ignore the connection.
- The new `refreshOnReconnect` option refreshes the current key when the connection returns, subject to `staleTime`. It defaults to `true`, except in `"always"` mode.
- `refreshOnFocus` and `refreshOnReconnect` accept `"always"`, to refresh even a fresh result (`Query.RefreshOn`).
- `QueryClient` takes the matching defaults: `defaultNetworkMode` and `defaultRefreshOnReconnect`.
- Focus and reconnect refreshes no longer restart a request that is already running, and a paused request resumes by itself.
The connection status comes from the new `AppConnectivity` service, and focus from the new `AppFocus` service. Each module provides `layerBrowser` (window events), `layerDummy` (always online or focused, e.g. for tests) and `make(view)` to build the service from any `View<boolean>`, e.g. on React Native.
- [`79d9061`](https://git.valverde.cloud/Thilawyn/effect-view/commit/79d906190754193b1b86bd07cd176d402180509f) - **Breaking:** `Mutation` takes a `networkMode` option too, defaulting to the client's `defaultNetworkMode`. Mutations called offline are now paused until the connection returns, unless `networkMode` is `"always"`. `Mutation.make` (and `MutationForm.make`) now require a `QueryClient`.
- [`79d9061`](https://git.valverde.cloud/Thilawyn/effect-view/commit/79d906190754193b1b86bd07cd176d402180509f) - **Breaking:** `Mutation` takes a `networkMode` option too, defaulting to the client's new `defaultMutationNetworkMode` (`"online"` by default). In the default `"online"` mode, mutations called offline are now paused until the connection returns. `Mutation.make` (and `MutationForm.make`) now require a `QueryClient`.
- [`2ed417a`](https://git.valverde.cloud/Thilawyn/effect-view/commit/2ed417a49f283c120af4034d1a496d3fe304dd05) - **Breaking:** `QueryClient.layer` and `QueryClient.layerWithOptions` now require the `AppConnectivity` and `AppFocus` services. In a browser, use `QueryClient.layerBrowser` and `QueryClient.layerBrowserWithOptions`, which provide both.
+1 -1
View File
@@ -78,7 +78,7 @@ Starting a mutation does not interrupt an earlier one — calls can overlap, eac
## Network mode
Like queries, `Mutation.make({ f, networkMode? })` takes a `networkMode` (falls back to the `QueryClient`'s `defaultNetworkMode`, `"online"` by default). In `"online"` mode a call made offline waits for the `AppConnectivity` service to report a connection before running `f`; meanwhile `state.paused` is `true` (with `result.waiting` still `true`). `"always"` ignores the connection.
Like queries, `Mutation.make({ f, networkMode? })` takes a `networkMode` (falls back to the `QueryClient`'s `defaultMutationNetworkMode`, `"online"` by default — separate from the queries' `defaultNetworkMode`). In `"online"` mode a call made offline waits for the `AppConnectivity` service to report a connection before running `f`; meanwhile `state.paused` is `true` (with `result.waiting` still `true`). `"offlineFirst"` runs `f` right away, and if it fails while offline, pauses until the connection returns and runs it again. `"always"` ignores the connection.
## Updating queries after a mutation
+5 -4
View File
@@ -12,7 +12,7 @@ effect-view's take on TanStack Query: reactive query keys, cached results, stale
| `fetchStatus: 'paused'` | `state.paused` |
| `refetch` | `query.refresh` / `query.refreshStream` |
| `invalidateQueries` | `query.invalidateCache` / `invalidateCacheEntry` |
| `networkMode` | `networkMode` (`"online"` / `"always"`) |
| `networkMode` | `networkMode` (`"online"` / `"offlineFirst"` / `"always"`) |
| `onlineManager` / `focusManager` | the `AppConnectivity` / `AppFocus` services |
## QueryClient: the shared cache
@@ -33,7 +33,7 @@ const appLayer = Layer.empty.pipe(
export const runtime = ReactRuntime.make(appLayer)
```
`QueryClient.layer` (defaults) / `QueryClient.layerWithOptions(options)` build the service and fork its background garbage-collection loop into the layer's scope. Individual queries may override `staleTime`/`networkMode`/`refreshOnFocus`/`refreshOnReconnect`; unset options fall back to these client defaults (`defaultStaleTime`, `defaultNetworkMode`, `defaultRefreshOnFocus`, `defaultRefreshOnReconnect`). `cacheGcTime` controls how long a stale, unaccessed cache entry is kept before eviction. They require the `AppConnectivity` and `AppFocus` services (see "Network mode and offline support"); `QueryClient.layerBrowser` / `QueryClient.layerBrowserWithOptions(options)` provide the browser implementations of both. You interact with the client only indirectly through `Query` instances — no need to call `QueryClientService` methods directly.
`QueryClient.layer` (defaults) / `QueryClient.layerWithOptions(options)` build the service and fork its background garbage-collection loop into the layer's scope. Individual queries may override `staleTime`/`networkMode`/`refreshOnFocus`/`refreshOnReconnect`; unset options fall back to these client defaults (`defaultStaleTime`, `defaultNetworkMode`, `defaultRefreshOnFocus`, `defaultRefreshOnReconnect`); mutations use `defaultMutationNetworkMode` instead of `defaultNetworkMode`. `cacheGcTime` controls how long a stale, unaccessed cache entry is kept before eviction. They require the `AppConnectivity` and `AppFocus` services (see "Network mode and offline support"); `QueryClient.layerBrowser` / `QueryClient.layerBrowserWithOptions(options)` provide the browser implementations of both. You interact with the client only indirectly through `Query` instances — no need to call `QueryClientService` methods directly.
## Create and run a query
@@ -114,13 +114,14 @@ const query = yield* Query.make(options).pipe(
- `staleTime` (per query, falls back to `QueryClient` default): how long a successful result satisfies a fetch without re-running `f`. A stale entry stays available as previous data while it refreshes.
- `cacheGcTime` (on `QueryClient`): entries unused for `staleTime + cacheGcTime` are evicted.
- `refreshOnFocus` (per query, falls back to `QueryClient` default): re-resolves the current key when the app regains focus, as reported by the `AppFocus` service (window `focus`/`blur` events with `AppFocus.layerBrowser`).
- `refreshOnFocus` (per query, falls back to `QueryClient` default): re-resolves the current key when the app regains focus, as reported by the `AppFocus` service (window `focus`/`blur` events with `AppFocus.layerBrowser`). `true` refreshes only a stale result, `"always"` regardless of `staleTime` (type `Query.RefreshOn`).
## Network mode and offline support
- `networkMode: "online"` (default): a request waits for a connection before running `f`; meanwhile `state.paused` is `true` (with `result.waiting` still `true`). Paused requests can be superseded/interrupted like any other, which clears `paused`.
- `networkMode: "offlineFirst"`: runs `f` right away (e.g. served by a service worker / HTTP cache); if it fails while offline, the request is `paused` until the connection returns, then `f` runs again. A failure while online settles normally.
- `networkMode: "always"`: ignores the connection (for queries that don't use the network).
- `refreshOnReconnect` (defaults to the client default in `"online"` mode, `false` otherwise): re-resolves the current key when the connection returns, subject to freshness. Focus/reconnect refreshes never restart an in-flight request (a paused one resumes by itself), as in TanStack Query.
- `refreshOnReconnect` (defaults to the client default, `false` in `"always"` mode): re-resolves the current key when the connection returns, subject to freshness (`"always"`: regardless of `staleTime`). Focus/reconnect refreshes never restart an in-flight request (a paused one resumes by itself), as in TanStack Query.
- The connection comes from the `AppConnectivity` service (`navigator.onLine` + window `online`/`offline` events with `AppConnectivity.layerBrowser`). `AppConnectivity` and `AppFocus` are `Context.Service`s and must be provided to `QueryClient.layer`/`layerWithOptions` (the `layerBrowser*` variants do it for you).
- Each module has `layerBrowser` (window events), `layerDummy` (always online / always focused; tests, SSR) and `make(view: View<boolean>)` to build the service from any `View`, provided with `Layer.succeed`. A `View`'s `changes` stream starts with the current value.
- React Native: build them from platform APIs:
+1 -1
View File
@@ -48,7 +48,7 @@ export const layerBrowser: Layer.Layer<AppConnectivity> = Layer.succeed(
Effect.sync(() => {
const onlineListener = () => Queue.offerUnsafe(queue, true)
globalThis.window.addEventListener("online", onlineListener)
const offlineListener = () => Queue.offerUnsafe(queue, true)
const offlineListener = () => Queue.offerUnsafe(queue, false)
globalThis.window.addEventListener("offline", offlineListener)
return [onlineListener, offlineListener] as const
+1 -1
View File
@@ -48,7 +48,7 @@ export const layerBrowser: Layer.Layer<AppFocus> = Layer.succeed(
Effect.sync(() => {
const focusListener = () => Queue.offerUnsafe(queue, true)
globalThis.window.addEventListener("focus", focusListener)
const blurListener = () => Queue.offerUnsafe(queue, true)
const blurListener = () => Queue.offerUnsafe(queue, false)
globalThis.window.addEventListener("blur", blurListener)
return [focusListener, blurListener] as const
+27
View File
@@ -247,4 +247,31 @@ describe("Mutation network mode", () => {
expect(expectSuccessValue(result)).toBe("value:1")
expect(result.paused).toBe(false)
})
it("uses the client's defaultMutationNetworkMode, not defaultNetworkMode", async () => {
const result = await Effect.runPromise(Effect.scoped(Effect.gen(function*() {
const mutation = yield* Mutation.make({
f: (key: number) => Effect.succeed(`value:${key}`),
})
return yield* mutation.mutate(1).pipe(Effect.timeout("1 second"))
}).pipe(
Effect.provide(QueryClient.layerWithOptions({
defaultNetworkMode: "online",
defaultMutationNetworkMode: "always",
}).pipe(
Layer.provide(Layer.mergeAll(
Layer.succeed(AppConnectivity.AppConnectivity, AppConnectivity.make(View.make({
get: Effect.succeed(false),
changes: Stream.make(false),
}))),
AppFocus.layerDummy,
)),
)),
)))
expect(expectSuccessValue(result)).toBe("value:1")
expect(result.paused).toBe(false)
})
})
+1 -1
View File
@@ -153,7 +153,7 @@ export const make = Effect.fnUntraced(function* <K = never, A = void, E = never,
yield* Effect.context<Scope.Scope | QueryClient.QueryClient | R>(),
),
options.f,
options.networkMode ?? client.defaultNetworkMode,
options.networkMode ?? client.defaultMutationNetworkMode,
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<K>())),
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Operation.Operation<Option.Some<K>, A, E, R>>())),
+33 -6
View File
@@ -32,8 +32,8 @@ extends Pipeable.Pipeable {
readonly stream: Stream.Stream<OperationState<K, A, E>>
/**
* Forks `effect` in the current scope, moving the state to waiting, then to the result of `effect`. In `"online"`
* network mode, while `online` is false the Operation is paused and waits before running `effect`.
* Forks `effect` in the current scope, moving the state to waiting, then to the result of `effect`. Depending on the
* network mode, the Operation may be paused while `online` is false (see `NetworkMode`).
* Returns the running fiber, or `None` if it has already completed.
*/
readonly start: Effect.Effect<Option.Option<Fiber.Fiber<A, E>>>
@@ -82,9 +82,11 @@ export interface OperationFinalState<out K, out A, out E = never> {
/**
* - `"online"`: an Operation waits for a network connection (see `AppConnectivity`) before running, and is `paused`
* meanwhile.
* - `"offlineFirst"`: an Operation runs once regardless of the network connection, e.g. for effects that may be served
* from a cache. If it fails while offline, it waits for a network connection, `paused` meanwhile, and runs again.
* - `"always"`: Operations ignore the network connection, e.g. for effects that do not use the network.
*/
export type NetworkMode = "online" | "always"
export type NetworkMode = "online" | "offlineFirst" | "always"
export const isOperation = (u: unknown): u is Operation<unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, OperationTypeId)
@@ -115,9 +117,7 @@ extends Pipeable.Class implements Operation<K, A, E, R> {
Effect.onExit(
Effect.andThen(
this.update(state => withWaiting(state, true)),
this.networkMode === "online"
? Effect.andThen(this.awaitOnline, this.effect)
: this.effect,
this.runWithNetworkMode,
),
exit => Effect.flatMap(
Lens.get(this.state),
@@ -138,6 +138,33 @@ extends Pipeable.Class implements Operation<K, A, E, R> {
)
}
// biome-ignore lint/suspicious/useGetterReturn: the switch is exhaustive, which TypeScript checks
get runWithNetworkMode(): Effect.Effect<A, E, AppConnectivity.AppConnectivity | R> {
switch (this.networkMode) {
case "online":
return Effect.andThen(this.awaitOnline, this.effect)
case "offlineFirst":
return this.runRetryingOnReconnect
case "always":
return this.effect
}
}
/** Runs `effect`, and if it fails while offline, waits for `online`, paused meanwhile, and runs it again. */
get runRetryingOnReconnect(): Effect.Effect<A, E, AppConnectivity.AppConnectivity | R> {
return Effect.catchCause(
this.effect,
cause => Effect.gen({ self: this }, function*() {
const { online } = yield* AppConnectivity.AppConnectivity
if (Cause.hasInterruptsOnly(cause) || (yield* View.get(online)))
return yield* Effect.failCause(cause)
yield* this.awaitOnline
return yield* this.runRetryingOnReconnect
}),
)
}
/** Waits for `online`, paused meanwhile. */
get awaitOnline(): Effect.Effect<void, never, AppConnectivity.AppConnectivity> {
return Effect.gen({ self: this }, function*() {
@@ -245,4 +245,153 @@ describe("Query network mode", () => {
expect(result).toEqual({ afterStart: 1, afterFocus: 2 })
})
it("in offlineFirst mode, a request made offline runs right away", async () => {
const result = await runQueryTest(withAppConnectivity(false, () => Effect.gen(function*() {
const query = yield* Query.make({
key: staticKey(1),
f: (id: number) => Effect.succeed(`value:${id}`),
networkMode: "offlineFirst",
})
return yield* query.fetch(1).pipe(Effect.timeout("1 second"))
})))
expect(AsyncResult.isSuccess(result.result)).toBe(true)
expect(result.paused).toBe(false)
})
it("in offlineFirst mode, a request that fails offline is paused, then runs again when the connection returns", async () => {
const result = await runQueryTest(withAppConnectivity(false, online => Effect.gen(function*() {
let calls = 0
const query = yield* Query.make({
key: staticKey(1),
f: (id: number) => Effect.suspend(() => {
calls += 1
return calls === 1 ? Effect.fail("offline") : Effect.succeed(`value:${id}`)
}),
networkMode: "offlineFirst",
})
const stream = yield* query.fetchStream(1)
const states = yield* Effect.forkScoped(Stream.runCollect(stream))
yield* Effect.yieldNow
const whileOffline = { calls, state: yield* View.get(query.state) }
yield* SubscriptionRef.set(online, true)
return {
whileOffline,
states: yield* Fiber.join(states).pipe(Effect.timeout("1 second")),
calls,
}
})))
expect(result.whileOffline.calls).toBe(1)
expect(result.whileOffline.state.paused).toBe(true)
expect(result.whileOffline.state.result.waiting).toBe(true)
expect(result.calls).toBe(2)
const last = result.states.at(-1)
expect(last && AsyncResult.isSuccess(last.result) && last.result.value).toBe("value:1")
expect(last?.paused).toBe(false)
})
it("in offlineFirst mode, a request that fails online is not run again", async () => {
const result = await runQueryTest(withAppConnectivity(true, () => Effect.gen(function*() {
let calls = 0
const query = yield* Query.make({
key: staticKey(1),
f: (_id: number) => Effect.suspend(() => {
calls += 1
return Effect.fail("error")
}),
networkMode: "offlineFirst",
})
return { final: yield* query.fetch(1).pipe(Effect.timeout("1 second")), calls }
})))
expect(AsyncResult.isFailure(result.final.result)).toBe(true)
expect(result.calls).toBe(1)
})
it("refreshes a fresh query on focus only when refreshOnFocus is \"always\"", async () => {
const result = await runQueryTest(withAppFocus(true, focused => Effect.gen(function*() {
const calls = { true: 0, always: 0 }
const makeQuery = (refreshOnFocus: true | "always") => Query.make({
key: staticKey(refreshOnFocus),
f: () => Effect.sync(() => { calls[refreshOnFocus === true ? "true" : "always"] += 1 }),
staleTime: "1 hour",
refreshOnFocus,
}).pipe(Query.thenRun)
yield* makeQuery(true)
yield* makeQuery("always")
yield* Effect.sleep("10 millis")
yield* SubscriptionRef.set(focused, false)
yield* SubscriptionRef.set(focused, true)
yield* Effect.sleep("10 millis")
return calls
})))
expect(result).toEqual({ true: 1, always: 2 })
})
it("refreshes a fresh query on reconnect when refreshOnReconnect is \"always\"", async () => {
const calls = await runQueryTest(withAppConnectivity(true, online => Effect.gen(function*() {
let calls = 0
yield* Query.make({
key: staticKey(1),
f: () => Effect.sync(() => { calls += 1 }),
staleTime: "1 hour",
refreshOnReconnect: "always",
}).pipe(Query.thenRun)
yield* Effect.sleep("10 millis")
yield* SubscriptionRef.set(online, false)
yield* SubscriptionRef.set(online, true)
yield* Effect.sleep("10 millis")
return calls
})))
expect(calls).toBe(2)
})
})
describe("browser layers", () => {
it("AppConnectivity.layerBrowser follows the online and offline events", async () => {
const values = await Effect.runPromise(Effect.gen(function*() {
const { online } = yield* AppConnectivity.AppConnectivity
const values = yield* Effect.forkScoped(Stream.runCollect(Stream.take(View.changes(online), 3)))
yield* Effect.sleep("10 millis")
window.dispatchEvent(new Event("offline"))
window.dispatchEvent(new Event("online"))
return yield* Fiber.join(values).pipe(Effect.timeout("1 second"))
}).pipe(
Effect.provide(AppConnectivity.layerBrowser),
Effect.scoped,
))
expect(values).toEqual([navigator.onLine, false, true])
})
it("AppFocus.layerBrowser follows the focus and blur events", async () => {
const values = await Effect.runPromise(Effect.gen(function*() {
const { focused } = yield* AppFocus.AppFocus
const values = yield* Effect.forkScoped(Stream.runCollect(Stream.take(View.changes(focused), 3)))
yield* Effect.sleep("10 millis")
window.dispatchEvent(new Event("blur"))
window.dispatchEvent(new Event("focus"))
return yield* Fiber.join(values).pipe(Effect.timeout("1 second"))
}).pipe(
Effect.provide(AppFocus.layerBrowser),
Effect.scoped,
))
expect(values).toEqual([document.hasFocus(), false, true])
})
})
+27 -15
View File
@@ -22,8 +22,8 @@ extends Pipeable.Pipeable {
readonly staleTime: Duration.Duration
readonly networkMode: Operation.NetworkMode
readonly refreshOnFocus: boolean
readonly refreshOnReconnect: boolean
readonly refreshOnFocus: RefreshOn
readonly refreshOnReconnect: RefreshOn
readonly operation: View.View<Option.Option<Operation.Operation<K, A, E, QueryClient.QueryClient | R>>>
readonly state: View.View<QueryState<K, A, E>>
@@ -56,6 +56,14 @@ export interface QueryFinalState<out K, out A, out E = never> {
readonly paused: false
}
/**
* When a Query refreshes its current key on an event, such as the app regaining focus or connection:
* - `true`: if the cached result is stale.
* - `"always"`: regardless of `staleTime`.
* - `false`: never.
*/
export type RefreshOn = boolean | "always"
export const isQuery = (u: unknown): u is Query<unknown, unknown, unknown, unknown> => Predicate.hasProperty(u, QueryTypeId)
@@ -71,8 +79,8 @@ extends Pipeable.Class implements Query<K, A, E, R> {
readonly staleTime: Duration.Duration,
readonly networkMode: Operation.NetworkMode,
readonly refreshOnFocus: boolean,
readonly refreshOnReconnect: boolean,
readonly refreshOnFocus: RefreshOn,
readonly refreshOnReconnect: RefreshOn,
readonly operation: Lens.Lens<Option.Option<Operation.Operation<K, A, E, QueryClient.QueryClient | R>>>,
readonly state: Lens.Lens<QueryState<K, A, E>>,
@@ -94,14 +102,14 @@ extends Pipeable.Class implements Query<K, A, E, R> {
this.refreshOnFocus
? Effect.flatMap(AppFocus.AppFocus, ({ focused }) => Stream.runForEach(
becomesTrue(focused),
() => this.refreshUnlessWaiting,
() => this.refreshUnlessWaiting(this.refreshOnFocus === "always"),
))
: Effect.void,
this.refreshOnReconnect
? Effect.flatMap(AppConnectivity.AppConnectivity, ({ online }) => Stream.runForEach(
becomesTrue(online),
() => this.refreshUnlessWaiting,
() => this.refreshUnlessWaiting(this.refreshOnReconnect === "always"),
))
: Effect.void,
], { concurrency: "unbounded" }).pipe(
@@ -162,14 +170,15 @@ extends Pipeable.Class implements Query<K, A, E, R> {
/**
* Refreshes the current key, unless a request is already in flight. Used when the app regains focus or connection:
* a request that is running keeps running, and one paused for lack of a connection resumes by itself.
* a request that is running keeps running, and one paused for lack of a connection resumes by itself. With
* `ignoreStaleTime`, a fresh cached result is refreshed too.
*/
get refreshUnlessWaiting(): Effect.Effect<void, Cause.NoSuchElementError, Query.Context<R>> {
refreshUnlessWaiting(ignoreStaleTime: boolean): Effect.Effect<void, Cause.NoSuchElementError, Query.Context<R>> {
return Effect.flatMap(
Lens.get(this.state),
state => state.result.waiting
? Effect.void
: Effect.asVoid(this.start(this.previousForLatestKey)),
: Effect.asVoid(this.start(this.previousForLatestKey, ignoreStaleTime)),
)
}
@@ -191,10 +200,11 @@ extends Pipeable.Class implements Query<K, A, E, R> {
/**
* Interrupts the current Operation and replaces it with a new one, started from `previous` or served from the
* cache.
* cache unless `ignoreStaleTime` is true.
*/
start(
previous: Effect.Effect<QueryState<K, A, E>>,
ignoreStaleTime = false,
): Effect.Effect<
Operation.Operation<K, A, E, QueryClient.QueryClient | R>,
Cause.NoSuchElementError,
@@ -205,7 +215,9 @@ extends Pipeable.Class implements Query<K, A, E, R> {
const { key, result } = yield* previous
const entry = yield* this.getCacheEntry(key)
const isFresh = Option.isSome(entry) && !(yield* QueryClient.isQueryClientCacheEntryStale(entry.value))
const isFresh = !ignoreStaleTime
&& Option.isSome(entry)
&& !(yield* QueryClient.isQueryClientCacheEntryStale(entry.value))
const effect = Effect.tap(this.f(key), v => this.setCacheEntry(key, AsyncResult.success(v)))
@@ -315,9 +327,9 @@ export declare namespace make {
readonly staleTime?: Duration.Input
readonly networkMode?: Operation.NetworkMode
readonly refreshOnFocus?: boolean
/** Defaults to the client's `defaultRefreshOnReconnect` in `"online"` network mode, and to `false` otherwise. */
readonly refreshOnReconnect?: boolean
readonly refreshOnFocus?: RefreshOn
/** Defaults to the client's `defaultRefreshOnReconnect`, and to `false` in `"always"` network mode. */
readonly refreshOnReconnect?: RefreshOn
}
}
@@ -343,7 +355,7 @@ export const make = Effect.fnUntraced(function* <K, A, E = never, R = never>(
options.staleTime ? yield* Effect.fromOption(Duration.fromInput(options.staleTime)) : client.defaultStaleTime,
networkMode,
options.refreshOnFocus ?? client.defaultRefreshOnFocus,
options.refreshOnReconnect ?? (networkMode === "online" && client.defaultRefreshOnReconnect),
options.refreshOnReconnect ?? (networkMode === "always" ? false : client.defaultRefreshOnReconnect),
Lens.fromSubscriptionRef(yield* SubscriptionRef.make(Option.none<Operation.Operation<K, A, E, QueryClient.QueryClient | R>>())),
Lens.fromSubscriptionRef(yield* SubscriptionRef.make<QueryState<K, A, E>>({
+13 -6
View File
@@ -4,6 +4,7 @@ import * as AppConnectivity from "./AppConnectivity.js"
import * as AppFocus from "./AppFocus.js"
import * as Lens from "./Lens.js"
import type * as Operation from "./Operation.js"
import type * as Query from "./Query.js"
import type * as View from "./View.js"
@@ -19,8 +20,9 @@ export interface QueryClientService extends Pipeable.Pipeable {
readonly defaultStaleTime: Duration.Duration
readonly defaultNetworkMode: Operation.NetworkMode
readonly defaultRefreshOnFocus: boolean
readonly defaultRefreshOnReconnect: boolean
readonly defaultRefreshOnFocus: Query.RefreshOn
readonly defaultRefreshOnReconnect: Query.RefreshOn
readonly defaultMutationNetworkMode: Operation.NetworkMode
readonly run: Effect.Effect<void, Cause.NoSuchElementError>
@@ -50,8 +52,9 @@ implements QueryClientService {
readonly defaultStaleTime: Duration.Duration,
readonly defaultNetworkMode: Operation.NetworkMode,
readonly defaultRefreshOnFocus: boolean,
readonly defaultRefreshOnReconnect: boolean,
readonly defaultRefreshOnFocus: Query.RefreshOn,
readonly defaultRefreshOnReconnect: Query.RefreshOn,
readonly defaultMutationNetworkMode: Operation.NetworkMode,
readonly runSemaphore: Semaphore.Semaphore,
) {
@@ -112,9 +115,12 @@ export declare namespace make {
export interface Options {
readonly cacheGcTime?: Duration.Input
readonly defaultStaleTime?: Duration.Input
/** The default network mode of queries. */
readonly defaultNetworkMode?: Operation.NetworkMode
readonly defaultRefreshOnFocus?: boolean
readonly defaultRefreshOnReconnect?: boolean
readonly defaultRefreshOnFocus?: Query.RefreshOn
readonly defaultRefreshOnReconnect?: Query.RefreshOn
/** The default network mode of mutations. */
readonly defaultMutationNetworkMode?: Operation.NetworkMode
}
}
@@ -137,6 +143,7 @@ export const make = Effect.fnUntraced(function* (
options.defaultNetworkMode ?? "online",
options.defaultRefreshOnFocus ?? true,
options.defaultRefreshOnReconnect ?? true,
options.defaultMutationNetworkMode ?? "online",
yield* Semaphore.make(1),
)