SubscriptionRef
A SubscriptionRef<A> is a Ref you can observe. It
holds the current value like any reference, but it also exposes a
Stream of changes: subscribe to it and you receive the current
value immediately, followed by every subsequent update.
This is the bridge between mutable state and reactive consumers. Use it for state that drives a UI, a live dashboard, a health indicator — anything that needs to react to changes rather than poll for them.
Observing changes
Section titled “Observing changes”The defining operation is changes, which returns a Stream that replays the
current value and then emits each future update.
import { Deferred, Effect, Fiber, Stream, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0) // Used to make sure the subscriber is attached before we start updating. const ready = yield* Deferred.make<void>()
// Fork a fiber that collects the first three values it observes. const fiber = yield* SubscriptionRef.changes(ref).pipe( // Signal "ready" as soon as the first (current) value arrives. Stream.tap(() => Deferred.succeed(ready, void 0)), Stream.take(3), Stream.runCollect, Effect.forkChild )
// Wait until the subscriber has received the initial value... yield* Deferred.await(ready) // ...then push two updates; both are published to the subscriber. yield* SubscriptionRef.set(ref, 1) yield* SubscriptionRef.set(ref, 2)
return yield* Fiber.join(fiber) // [0, 1, 2]})The subscriber sees 0 (the value at the moment it subscribed), then 1 and 2.
A subscriber that attaches later would still receive the latest value first,
because SubscriptionRef replays the most recent value to every new subscriber.
Updating the value
Section titled “Updating the value”SubscriptionRef carries the full Ref-style update API, and every successful
update is published to current subscribers. Because updates run under an internal
semaphore, the effectful variants (updateEffect, modifyEffect, …) are
serialized the same way SynchronizedRef
serializes them.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10)
// Pure update — publishes the new value (20) to subscribers. yield* SubscriptionRef.update(ref, (n) => n * 2)
// Effectful update — serialized, and also published once it commits. yield* SubscriptionRef.updateEffect(ref, (n) => Effect.succeed(n + 5))
return yield* SubscriptionRef.get(ref) // 25})Reactive shared state through a service
Section titled “Reactive shared state through a service”The natural home for a SubscriptionRef is a service
that owns a piece of shared state and lets the rest of the app both mutate it and
subscribe to it. Here a service tracks application status; a background worker
flips the status while a logger reacts to every transition.
import { Context, Effect, Fiber, Layer, Stream, SubscriptionRef } from "effect"
type Status = "starting" | "ready" | "degraded"
class Health extends Context.Service<Health, { readonly set: (status: Status) => Effect.Effect<void> readonly changes: Stream.Stream<Status>}>()("app/Health") { static layer = Layer.effect( Health, Effect.gen(function*() { const ref = yield* SubscriptionRef.make<Status>("starting") return Health.of({ set: (status) => SubscriptionRef.set(ref, status), // Expose the change stream so any consumer can react to transitions. changes: SubscriptionRef.changes(ref) }) }) )}
const program = Effect.gen(function*() { const health = yield* Health
// A consumer that logs every status it observes, including the initial one. const logger = yield* health.changes.pipe( Stream.take(3), Stream.runForEach((status) => Effect.log(`status -> ${status}`)), Effect.forkChild )
// Drive some transitions; each is published to the logger above. yield* health.set("ready") yield* health.set("degraded")
yield* Fiber.join(logger)}).pipe(Effect.provide(Health.layer))The logger prints starting, then ready, then degraded — it reacts to state
changes without ever polling, and any number of consumers can subscribe
independently.
When to use it
Section titled “When to use it”Choose SubscriptionRef when something needs to observe state as it evolves. If
you only ever read the current value, a plain Ref is
simpler and lighter. If you need serialized effectful updates but no subscribers,
use SynchronizedRef. For everything you
can build on top of the change stream, see Streaming.
Reference
Section titled “Reference”A SubscriptionRef<A> has the same operation surface as
SynchronizedRef — the full set of pure
operations plus their *Effect variants, all serialized under an internal
semaphore so concurrent updates never interleave — plus the observable
changes stream. Every successful set, every non-empty update,
and every committed modify publishes the new value to current subscribers.
isSubscriptionRef
Section titled “isSubscriptionRef”Type guard that returns true when the value is a SubscriptionRef. Useful for
narrowing an unknown before calling subscription-ref operations.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0)
SubscriptionRef.isSubscriptionRef(ref) // => true SubscriptionRef.isSubscriptionRef(42) // => false})Constructs a SubscriptionRef from an initial value. The initial value is
published during construction, so changes starts new subscribers
with it.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0) return yield* SubscriptionRef.get(ref) // => 0})changes
Section titled “changes”Returns a Stream that first emits the current value and then
emits every subsequent published value. Each new subscriber independently
receives the latest value first thanks to the replay buffer.
import { Effect, Fiber, Stream, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0)
// Subscribe (replays the current value 0), then drive an update. const fiber = yield* SubscriptionRef.changes(ref).pipe( Stream.take(2), Stream.runCollect, Effect.forkChild ) yield* SubscriptionRef.set(ref, 1)
return yield* Fiber.join(fiber) // => [0, 1]})Reads the current value once, without subscribing. Equivalent to Ref.get.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(42) return yield* SubscriptionRef.get(ref) // => 42})getUnsafe
Section titled “getUnsafe”Reads the current value synchronously, bypassing the semaphore. Only safe when the caller is certain there are no concurrent modifications.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(42) return SubscriptionRef.getUnsafe(ref) // => 42 (plain value, not an Effect)})Replaces the value and publishes it to subscribers.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0) yield* SubscriptionRef.set(ref, 42) return yield* SubscriptionRef.get(ref) // => 42})getAndSet
Section titled “getAndSet”Sets a new value and returns the previous value, publishing the change.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.getAndSet(ref, 20) // => 10 (old value)})setAndGet
Section titled “setAndGet”Sets a new value and returns that new value, publishing the change.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0) return yield* SubscriptionRef.setAndGet(ref, 42) // => 42})update
Section titled “update”Applies a function to the current value and publishes the result.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) yield* SubscriptionRef.update(ref, (n) => n * 2) return yield* SubscriptionRef.get(ref) // => 20})updateAndGet
Section titled “updateAndGet”Applies a function and returns the new value, publishing the change.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.updateAndGet(ref, (n) => n * 2) // => 20})getAndUpdate
Section titled “getAndUpdate”Applies a function and returns the previous value, publishing the change.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.getAndUpdate(ref, (n) => n * 2) // => 10 (old value)})updateSome
Section titled “updateSome”Applies a partial update. On Option.some the value is set and published; on
Option.none the reference is left unchanged and nothing is published.
import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) yield* SubscriptionRef.updateSome( ref, (n) => n > 5 ? Option.some(n * 2) : Option.none() ) return yield* SubscriptionRef.get(ref) // => 20})updateSomeAndGet
Section titled “updateSomeAndGet”Like updateSome, but returns the value after the update
decision — the new value on some, or the unchanged current value on none.
import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.updateSomeAndGet( ref, (n) => n > 5 ? Option.some(n * 2) : Option.none() ) // => 20})getAndUpdateSome
Section titled “getAndUpdateSome”Like updateSome, but returns the previous value regardless
of whether the update was applied.
import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.getAndUpdateSome( ref, (n) => n > 5 ? Option.some(n * 2) : Option.none() ) // => 10 (old value)})modify
Section titled “modify”Computes a [result, newValue] pair: the new value is set and published, and the
separate result is returned. Use it to read and write in one atomic step.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.modify(ref, (n) => [`was ${n}`, n * 2]) // => "was 10" (ref now holds 20)})modifySome
Section titled “modifySome”Like modify, but the second element of the pair is an Option. On
Option.some the value is updated and published; on Option.none only the
result is returned and nothing is published.
import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.modifySome( ref, (n) => n > 5 ? ["updated", Option.some(n * 2)] : ["unchanged", Option.none()] ) // => "updated" (ref now holds 20)})updateEffect
Section titled “updateEffect”Effectful variant of update. The effect runs under the semaphore;
the produced value is set and published once it commits.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) yield* SubscriptionRef.updateEffect(ref, (n) => Effect.succeed(n + 5)) return yield* SubscriptionRef.get(ref) // => 15})updateAndGetEffect
Section titled “updateAndGetEffect”Effectful variant of updateAndGet — returns the new value.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.updateAndGetEffect( ref, (n) => Effect.succeed(n + 5) ) // => 15})getAndUpdateEffect
Section titled “getAndUpdateEffect”Effectful variant of getAndUpdate — returns the previous value.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.getAndUpdateEffect( ref, (n) => Effect.succeed(n + 5) ) // => 10 (old value)})updateSomeEffect
Section titled “updateSomeEffect”Effectful variant of updateSome. On effectful Option.some the
value is set and published; on Option.none nothing changes.
import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) yield* SubscriptionRef.updateSomeEffect( ref, (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) ) return yield* SubscriptionRef.get(ref) // => 13})updateSomeAndGetEffect
Section titled “updateSomeAndGetEffect”Effectful variant of updateSomeAndGet — returns the value
after the update decision.
import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.updateSomeAndGetEffect( ref, (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) ) // => 13})getAndUpdateSomeEffect
Section titled “getAndUpdateSomeEffect”Effectful variant of getAndUpdateSome — returns the
previous value.
import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.getAndUpdateSomeEffect( ref, (n) => Effect.succeed(n > 5 ? Option.some(n + 3) : Option.none()) ) // => 10 (old value)})modifyEffect
Section titled “modifyEffect”Effectful variant of modify. The effect yields a [result, newValue]
pair; the new value is set and published, the result returned.
import { Effect, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.modifyEffect( ref, (n) => Effect.succeed([`doubled ${n}`, n * 2] as const) ) // => "doubled 10" (ref now holds 20)})modifySomeEffect
Section titled “modifySomeEffect”Effectful variant of modifySome. The effect yields a
[result, Option<newValue>] pair; the value is published only on Option.some.
import { Effect, Option, SubscriptionRef } from "effect"
const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(10) return yield* SubscriptionRef.modifySomeEffect( ref, (n) => Effect.succeed( n > 5 ? (["updated", Option.some(n + 5)] as const) : (["unchanged", Option.none()] as const) ) ) // => "updated" (ref now holds 15)})