Resource
A Resource<A, E> keeps a single value loaded in memory and lets you reload it
on demand or on a schedule. It is the right tool when you have something that is
expensive to acquire and changes occasionally — an auth token, a feature-flag
snapshot, a remote configuration, a signing key — and you want every reader to
see the latest loaded value without re-running the acquisition each time.
Internally a Resource wraps an acquisition effect whose latest result is stored
in a scoped reference. Reading it returns the current value; refreshing re-runs
acquisition and atomically swaps in the new result, releasing any resources held
by the previous one.
Manual refresh
Section titled “Manual refresh”Resource.manual builds a resource you refresh yourself. Creation runs the
acquisition once and stores the result; Resource.get reads it; Resource.refresh
reloads it.
import { Effect, Resource } from "effect"
const program = Effect.gen(function*() { let version = 0
// Acquire a config snapshot. In a real app this would read from a file // or a remote service; here we just bump a counter so refreshes are // observable. const resource = yield* Resource.manual( Effect.sync(() => ({ version: ++version })) )
const first = yield* Resource.get(resource) // { version: 1 } const cached = yield* Resource.get(resource) // { version: 1 } (no reload)
yield* Resource.refresh(resource) // re-runs acquisition const reloaded = yield* Resource.get(resource) // { version: 2 }
return { first, cached, reloaded }}).pipe(Effect.scoped)Automatic refresh on a schedule
Section titled “Automatic refresh on a schedule”Resource.auto does the same as manual, but also forks a background fiber that
refreshes the resource according to a Schedule. The refresh
loop runs in the resource’s scope and stops when that scope closes.
import { Effect, Resource, Schedule } from "effect"
interface Config { readonly featureEnabled: boolean}
// Load remote config now, then reload it every 5 minutes in the// background. Readers always see the most recently loaded snapshot.const makeConfig = (load: Effect.Effect<Config, string>) => Resource.auto(load, Schedule.fixed("5 minutes"))
const program = Effect.gen(function*() { const config = yield* makeConfig(Effect.succeed({ featureEnabled: true }))
// Reads are cheap and never trigger acquisition themselves — they just // return whatever the background refresh last loaded. const current = yield* Resource.get(config) return current}).pipe(Effect.scoped)This pairs naturally with the service/Layer style: build the Resource once in a
scoped layer and expose get to the rest of the application.
import { Context, Effect, Layer, Resource, Schedule } from "effect"
interface Token { readonly value: string}
// A service that keeps a short-lived auth token fresh. The token is// acquired at startup and re-acquired every 50 minutes; callers just ask// for the current one.class Auth extends Context.Service<Auth, { readonly token: Effect.Effect<Token>}>()("app/Auth") { // Layer.effect runs construction in the layer's own scope, so the // resource's background refresh fiber is torn down when the layer closes. static layer = Layer.effect( Auth, Effect.gen(function*() { const resource = yield* Resource.auto( // Pretend this calls an identity provider. Effect.succeed<Token>({ value: "secret" }), Schedule.fixed("50 minutes") ) return Auth.of({ token: Resource.get(resource) }) }) )}Because the resource is created with Layer.effect (which runs construction in
the layer’s scope), its background refresh fiber is torn down automatically when
the layer’s scope closes.
Handling acquisition failures
Section titled “Handling acquisition failures”A Resource<A, E> stores the acquisition Exit — including failures. If
acquisition fails, Resource.get fails with that error until a later refresh
succeeds.
Resource.getreads the current stored result and fails withEif the last acquisition failed.Resource.refreshre-runs acquisition. If the new acquisition succeeds, it replaces the stored value (releasing the previous one’s resources). If it fails, the refresh effect fails — and with automatic refresh, the previously stored value is left in place forgetto keep reading.
import { Effect, Resource } from "effect"
const program = Effect.gen(function*() { let attempt = 0
// Fails on the first acquisition, succeeds afterwards. const resource = yield* Resource.manual( Effect.suspend(() => ++attempt === 1 ? Effect.fail("initial load failed" as const) : Effect.succeed("ok") ) )
// The stored result is a failure, so get fails too. const failed = yield* Effect.exit(Resource.get(resource)) // Exit.fail("initial load failed")
yield* Resource.refresh(resource) // succeeds this time const ok = yield* Resource.get(resource) // "ok"
return { failed, ok }}).pipe(Effect.scoped)