Reference Counting
Sometimes the thing you want to reuse is not a value but a resource: a database connection, an HTTP client, a worker, a websocket. Many fibers may need it at once, it should be acquired only once, and it must be released — but only after the last user is done with it. That is exactly what reference counting provides.
RcRef<A, E>shares a single scoped resource.RcMap<K, A, E>shares scoped resources keyed byK.
Both are lazy (the resource is acquired on first get), reference-counted (each
borrowing scope adds a reference and a release finalizer), and self-cleaning (the
resource is finalized when the count hits zero, optionally after an idle delay).
RcRef: one shared resource
Section titled “RcRef: one shared resource”RcRef.make takes an acquire effect — typically an
Effect.acquireRelease that defines how to open and
close the resource. RcRef.get borrows the current resource for the caller’s
scope, acquiring it the first time it is needed.
import { Effect, RcRef } from "effect"
// `acquire` describes how to open and close one connection. RcRef makes// sure it is opened at most once and closed when the last borrower leaves.const program = Effect.gen(function*() { const ref = yield* RcRef.make({ acquire: Effect.acquireRelease( Effect.as(Effect.log("connecting"), { id: 1 }), () => Effect.log("disconnecting") ) })
// Two borrows in nested scopes: the connection is acquired once for the // first get and reused for the second. It is released only when both // borrowing scopes have closed. yield* Effect.scoped( Effect.gen(function*() { const a = yield* RcRef.get(ref) const b = yield* RcRef.get(ref) // a === b: the same acquired resource }) )}).pipe(Effect.scoped)Note the two scopes at play. RcRef.make captures the surrounding scope (here
the outer Effect.scoped) — that is the resource’s ultimate owner. Each
RcRef.get requires a Scope of its own (the inner Effect.scoped); the
reference is held for as long as that inner scope is open.
Idle time-to-live and invalidation
Section titled “Idle time-to-live and invalidation”By default, the moment the reference count drops to zero the resource is
released. Pass idleTimeToLive to keep it alive for a grace period, so a quick
re-acquisition reuses the still-open resource instead of paying to reopen it:
import { Effect, RcRef } from "effect"
const makeClientRef = RcRef.make({ acquire: Effect.acquireRelease( Effect.succeed({ requests: 0 }), () => Effect.log("closing client") ), // Keep the client around for 30 seconds after the last borrower // releases it, in case another request comes in soon. idleTimeToLive: "30 seconds"})RcRef.invalidate forces the next get to acquire a fresh resource — useful
when the current one is stale or broken. It does not revoke resources already
borrowed by active scopes; those keep using the old value until their scopes
close.
RcMap: shared resources by key
Section titled “RcMap: shared resources by key”RcMap is the keyed version: one acquired resource per key, each independently
reference-counted. Reach for it when you pool resources by some identifier — a
connection per database, a client per region, a session per tenant.
import { Effect, RcMap } from "effect"
const program = Effect.gen(function*() { // One connection per database name. Idle connections are released // 5 minutes after their last reference; at most 10 are held at once. const connections = yield* RcMap.make({ lookup: (dbName: string) => Effect.acquireRelease( Effect.as(Effect.log(`open ${dbName}`), { dbName }), () => Effect.log(`close ${dbName}`) ), idleTimeToLive: "5 minutes", capacity: 10 })
// Getting the same key twice acquires the resource once and shares it. yield* Effect.scoped( Effect.gen(function*() { yield* RcMap.get(connections, "orders") yield* RcMap.get(connections, "orders") // reuses the same connection yield* RcMap.get(connections, "billing") // a different connection }) )}).pipe(Effect.scoped)As with RcRef, the map itself is scoped (it releases every remaining entry when
its owning scope closes), and each RcMap.get requires a Scope that holds the
reference.
Capacity, idle TTL, and explicit control
Section titled “Capacity, idle TTL, and explicit control”RcMap.make accepts the same idleTimeToLive (a duration or a function of the
key) plus an optional capacity:
capacitybounds how many keys can be live at once. Exceeding it makesRcMap.getfail with aCause.ExceededCapacityErrorrather than evicting an in-use resource — reference counting means the map cannot safely drop something still in use.RcMap.invalidate(map, key)removes a key; if nothing is currently using it, it is released immediately, and the nextgetre-acquires it.RcMap.touch(map, key)resets the idle timer for a key, keeping an otherwise idle resource alive longer.RcMap.keys(map)andRcMap.has(map, key)inspect the current contents.
A typical use is a service that hands out pooled resources:
import { Context, Effect, Layer, RcMap, Scope } from "effect"
interface Connection { readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>>}
// A connection pool keyed by database name. Callers borrow a connection// for the duration of their own scope; the pool reuses and releases// connections under the hood.class Pool extends Context.Service<Pool, { readonly get: (db: string) => Effect.Effect<Connection, never, Scope.Scope>}>()("app/Pool") { // Layer.effect runs construction in the layer's own scope, so the // RcMap (and everything it holds) is torn down when the layer closes. static layer = Layer.effect( Pool, Effect.gen(function*() { const map = yield* RcMap.make({ lookup: (db: string) => Effect.acquireRelease( Effect.succeed<Connection>({ query: (_sql) => Effect.succeed([]) }), () => Effect.log(`closing pool connection for ${db}`) ), idleTimeToLive: "2 minutes" })
return Pool.of({ get: (db) => RcMap.get(map, db) }) }) )}RcRef and RcMap vs. Cache
Section titled “RcRef and RcMap vs. Cache”Reference counting is about resources and their lifecycle, not about memoizing computed values:
- Use
Cache/ScopedCachewhen you want to remember the result of a lookup (including failures), with TTL-based and capacity-based eviction. - Use
RcRef/RcMapwhen you want to share a live resource and have it released precisely when the last borrower is done, with idle grace periods.
If you need a single value kept loaded in memory and refreshed periodically, see
Resource.