2 Commits

Author SHA1 Message Date
renovate-bot 112e91d199 Update actions/checkout action to v7
Lint / lint (push) Failing after 37s
Test build / test-build (pull_request) Successful in 2m13s
2026-07-30 12:06:44 +00:00
Julien Valverdé e412943e64 Add Query.withScheduledRefresh
Lint / lint (push) Successful in 1m53s
2026-07-30 13:09:24 +02:00
6 changed files with 120 additions and 5 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Clone repo
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Lint TypeScript
+8 -1
View File
@@ -21,7 +21,7 @@ jobs:
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
- name: Clone repo
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install dependencies
run: bun install --frozen-lockfile
@@ -40,6 +40,13 @@ jobs:
access: public
token: ${{ secrets.NPM_TOKEN }}
registry: https://registry.npmjs.org
- name: Publish @effect-view/vite-plugin
uses: JS-DevTools/npm-publish@v4
with:
package: packages/vite-plugin
access: public
token: ${{ secrets.NPM_TOKEN }}
registry: https://registry.npmjs.org
- name: Publish effect-fc
uses: JS-DevTools/npm-publish@v4
with:
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: docker/setup-buildx-action@v3
- name: Clone repo
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install dependencies
run: bun install --frozen-lockfile
+27
View File
@@ -248,6 +248,33 @@ workflows that need to wait for the final success or failure state.
Invalidating does not itself refetch. Follow it with `refreshView`, change the
key, or allow a later fetch to repopulate the cache.
### Refresh on an interval
Refresh every five minutes, starting after five minutes:
```ts
import { Schedule } from "effect"
yield* query.pipe(
Query.withScheduledRefresh(Schedule.spaced("5 minutes")),
)
```
Limit the number of refreshes:
```ts
yield* query.pipe(
Query.withScheduledRefresh(
Schedule.spaced("5 minutes").pipe(
Schedule.upTo({ times: 3 }),
),
),
)
```
The refresh fiber stops with the surrounding scope. The Effect returns the
original query. Cache and `staleTime` rules still apply.
## Staleness and cache lifetime
`staleTime` controls how long a successful result can satisfy a fetch without
+45 -1
View File
@@ -1,4 +1,5 @@
import { Effect, type Scope, Stream } from "effect"
import { Effect, Schedule, type Scope, Stream } from "effect"
import { TestClock } from "effect/testing"
import { AsyncResult } from "effect/unstable/reactivity"
import { describe, expect, it } from "vitest"
import * as Query from "./Query.js"
@@ -79,6 +80,49 @@ describe("Query", () => {
expect(expectSuccessValue(result[1])).toBe("value:1:2")
})
it("withScheduledRefresh lets the Schedule control the first refresh", async () => {
let calls = 0
const key = staticKey<readonly [number]>([1])
const result = await runQueryTest(Effect.gen(function*() {
const query = yield* Query.make({
key,
f: () => Effect.sync(() => {
calls += 1
return calls
}),
staleTime: "0 millis",
})
yield* query.fetch([1])
const returnedQuery = yield* query.pipe(Query.withScheduledRefresh(
Schedule.spaced("1 second").pipe(
Schedule.upTo({ times: 1 }),
),
))
yield* TestClock.adjust("999 millis")
const beforeInterval = calls
yield* TestClock.adjust("1 millis")
const afterFirstInterval = calls
return {
returnsQuery: returnedQuery === query,
beforeInterval,
afterFirstInterval,
}
}).pipe(
Effect.provide(TestClock.layer()),
))
expect(result).toEqual({
returnsQuery: true,
beforeInterval: 1,
afterFirstInterval: 2,
})
})
it("invalidateCacheEntry forces the next fetch for that key to rerun", async () => {
let calls = 0
const key = staticKey<readonly [number]>([1])
+38 -1
View File
@@ -1,4 +1,4 @@
import { Cause, type Context, Duration, Effect, Equal, type Equivalence, Exit, Fiber, Option, Pipeable, Predicate, PubSub, Ref, type Scope, Semaphore, Stream, SubscriptionRef } from "effect"
import { Cause, type Context, Duration, Effect, Equal, type Equivalence, Exit, Fiber, Function, Option, Pipeable, Predicate, PubSub, Ref, type Schedule, type Scope, Semaphore, Stream, SubscriptionRef } from "effect"
import { AsyncResult } from "effect/unstable/reactivity"
import * as Lens from "./Lens.js"
import * as QueryClient from "./QueryClient.js"
@@ -421,6 +421,43 @@ export const service = <K, A, E = never, R = never>(
query => Effect.forkScoped(query.run),
)
/**
* Refreshes the Query on a schedule and returns it.
*
* @example Refresh every five minutes, starting after five minutes
* ```ts
* yield* query.pipe(
* Query.withScheduledRefresh(Schedule.spaced("5 minutes")),
* )
* ```
*
* @example Refresh at most three times
* ```ts
* yield* query.pipe(
* Query.withScheduledRefresh(
* Schedule.spaced("5 minutes").pipe(Schedule.upTo({ times: 3 })),
* ),
* )
* ```
*/
export const withScheduledRefresh: {
<Output, Error, Env>(
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
): <K, A, E, R>(
self: Query<K, A, E, R>,
) => Effect.Effect<Query<K, A, E, R>, never, Scope.Scope | Env>
<K, A, E, R, Output, Error, Env>(
self: Query<K, A, E, R>,
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
): Effect.Effect<Query<K, A, E, R>, never, Scope.Scope | Env>
} = Function.dual(2, <K, A, E, R, Output, Error, Env>(
self: Query<K, A, E, R>,
schedule: Schedule.Schedule<Output, unknown, Error, Env>,
) => Effect.schedule(self.refresh, schedule).pipe(
Effect.forkScoped,
Effect.as(self),
))
export class QueryStateLens<in out K, in out A, in out E = never>
extends Lens.LensImpl<QueryState<K, A, E>, never, never, never, never> {