Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tim-smart/effect-atom/llms.txt
Use this file to discover all available pages before exploring further.
What are scoped effects?
Scoped effects in Effect Atom automatically manage resources and cleanup. When you create an atom with an Effect, it receives a Scope that you can use to:
- Add finalizers that run when the atom is rebuilt or disposed
- Acquire resources that are automatically released
- Compose scoped effects from the Effect ecosystem
This ensures resources are properly cleaned up and prevents memory leaks.
Automatic scope provision
Every effect-based atom automatically gets a Scope:
import { Atom } from "@effect-atom/atom"
import { Effect, Scope } from "effect"
const resourceAtom = Atom.make(
Effect.gen(function* () {
// Scope is automatically provided
const scope = yield* Scope.Scope
// Or use scoped operations directly
const resource = yield* acquireResource()
return resource.data
})
)
Using finalizers
Finalizers are cleanup functions that run when an atom is rebuilt or disposed.
Basic finalizers
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const timerAtom = Atom.make(
Effect.gen(function* () {
const intervalId = setInterval(() => {
console.log("tick")
}, 1000)
// Cleanup when atom rebuilds or disposes
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
clearInterval(intervalId)
console.log("Timer cleaned up")
})
)
return "timer running"
})
)
Multiple finalizers
You can add multiple finalizers - they run in reverse order:
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const resourceAtom = Atom.make(
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Effect.log("First finalizer")
)
const resource = yield* openResource()
yield* Effect.addFinalizer(() =>
Effect.sync(() => resource.close()).pipe(
Effect.tap(() => Effect.log("Second finalizer"))
)
)
yield* Effect.addFinalizer(() =>
Effect.log("Third finalizer")
)
return resource.data
})
)
// When cleaned up, logs:
// Third finalizer
// Second finalizer
// First finalizer
Context-based finalizers
Use get.addFinalizer for non-effect cleanup:
import { Atom } from "@effect-atom/atom"
const eventAtom = Atom.make((get) => {
const handler = () => console.log("Event fired")
window.addEventListener("resize", handler)
// Cleanup using get.addFinalizer
get.addFinalizer(() => {
window.removeEventListener("resize", handler)
})
return "listening"
})
When finalizers run
Finalizers execute in these situations:
1. Atom rebuild
When dependencies change and the atom re-evaluates:
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const userIdAtom = Atom.make(1)
const userAtom = Atom.make((get) =>
Effect.gen(function* () {
const userId = get(userIdAtom)
const subscription = yield* subscribeToUser(userId)
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
subscription.unsubscribe()
console.log(`Unsubscribed from user ${userId}`)
})
)
return subscription.data
})
)
// When userIdAtom changes:
// 1. Finalizer runs (unsubscribes from old user)
// 2. Effect re-runs (subscribes to new user)
2. Atom disposal
When the atom is no longer used and not marked with keepAlive:
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const tempResourceAtom = Atom.make(
Effect.gen(function* () {
const resource = yield* acquireResource()
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
resource.release()
console.log("Resource released")
})
)
return resource
})
)
// Finalizer runs when no components use this atom
const persistentAtom = tempResourceAtom.pipe(
Atom.keepAlive
)
// Finalizer only runs when explicitly disposed
3. Manual interruption
When an effect is interrupted:
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const longRunningAtom = Atom.fn((id: number) =>
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Effect.log("Interrupted!")
)
yield* Effect.sleep(10000)
return `Completed ${id}`
})
)
// Start operation
registry.set(longRunningAtom, 1)
// Interrupt it
registry.set(longRunningAtom, Atom.Interrupt)
// Logs: "Interrupted!"
Resource management patterns
WebSocket connection
import { Atom } from "@effect-atom/atom"
import { Effect, Queue, Stream } from "effect"
const websocketAtom = Atom.make(
Effect.gen(function* () {
const queue = yield* Queue.unbounded<string>()
const ws = new WebSocket("wss://example.com")
ws.onmessage = (event) => {
Effect.runSync(Queue.offer(queue, event.data))
}
// Cleanup connection
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
ws.close()
console.log("WebSocket closed")
})
)
// Return stream of messages
return Stream.fromQueue(queue)
})
)
File handle
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
import * as fs from "fs/promises"
const fileAtom = Atom.make(
Effect.gen(function* () {
const handle = yield* Effect.tryPromise(() =>
fs.open("/path/to/file", "r")
)
yield* Effect.addFinalizer(() =>
Effect.tryPromise(() => handle.close()).pipe(
Effect.tap(() => Effect.log("File closed"))
)
)
const content = yield* Effect.tryPromise(() =>
handle.readFile({ encoding: "utf-8" })
)
return content
})
)
Database connection
import { Atom } from "@effect-atom/atom"
import { Effect, Context, Layer } from "effect"
class Database extends Context.Tag("Database")<
Database,
{ readonly query: (sql: string) => Effect.Effect<any> }
>() {}
const DatabaseLive = Layer.scoped(
Database,
Effect.gen(function* () {
const connection = yield* connectToDatabase()
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
connection.close()
console.log("Database connection closed")
})
)
return {
query: (sql: string) =>
Effect.tryPromise(() => connection.query(sql))
}
})
)
const dbRuntime = Atom.runtime(DatabaseLive)
Scoped services
Use Effect’s scoped services in atoms:
import { Atom } from "@effect-atom/atom"
import { Effect, Context, Layer } from "effect"
class Cache extends Context.Tag("Cache")<
Cache,
{ readonly get: (key: string) => Effect.Effect<string | null> }
>() {}
const CacheLive = Layer.scoped(
Cache,
Effect.gen(function* () {
const cache = new Map<string, string>()
// Cleanup on scope close
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
cache.clear()
console.log("Cache cleared")
})
)
return {
get: (key: string) => Effect.sync(() => cache.get(key) ?? null)
}
})
)
const runtime = Atom.runtime(CacheLive)
const dataAtom = runtime.atom(
Effect.gen(function* () {
const cache = yield* Cache
return yield* cache.get("key")
})
)
Nested scopes
Scopes can be nested for fine-grained control:
import { Atom } from "@effect-atom/atom"
import { Effect, Scope } from "effect"
const nestedAtom = Atom.make(
Effect.gen(function* () {
// Outer scope (atom lifetime)
yield* Effect.addFinalizer(() =>
Effect.log("Outer cleanup")
)
// Create inner scope
const innerScope = yield* Scope.make()
yield* Scope.extend(
Effect.gen(function* () {
// Inner scope
yield* Effect.addFinalizer(() =>
Effect.log("Inner cleanup")
)
return "inner resource"
}),
innerScope
)
// Close inner scope early
yield* Scope.close(innerScope, Exit.void)
// Logs: "Inner cleanup"
return "outer resource"
})
)
// When atom disposes:
// Logs: "Outer cleanup"
Effect.acquireRelease
Use Effect’s built-in resource management:
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const resourceAtom = Atom.make(
Effect.acquireRelease(
// Acquire
Effect.sync(() => {
console.log("Acquiring resource")
return { data: "resource" }
}),
// Release
(resource) => Effect.sync(() => {
console.log("Releasing resource")
resource.data = ""
})
).pipe(
Effect.map((resource) => resource.data)
)
)
Combining with get.addFinalizer
Combine Effect finalizers with context finalizers:
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const combinedAtom = Atom.make((get) =>
Effect.gen(function* () {
// Effect finalizer
yield* Effect.addFinalizer(() =>
Effect.log("Effect cleanup")
)
// Context finalizer (non-effectful)
get.addFinalizer(() => {
console.log("Context cleanup")
})
return "data"
})
)
Context finalizers run before Effect finalizers during cleanup.
Common patterns
Subscription management
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const topicAtom = Atom.make("news")
const subscriptionAtom = Atom.make((get) =>
Effect.gen(function* () {
const topic = get(topicAtom)
const sub = yield* subscribeTo(topic)
yield* Effect.addFinalizer(() =>
Effect.sync(() => sub.unsubscribe())
)
return sub.messages
})
)
// Automatically unsubscribes and resubscribes when topic changes
Interval cleanup
import { Atom } from "@effect-atom/atom"
import { Effect } from "effect"
const pollingAtom = Atom.make((get) => {
const intervalId = setInterval(() => {
get.refreshSelf()
}, 5000)
get.addFinalizer(() => {
clearInterval(intervalId)
})
return Effect.succeed(Date.now())
})
Observer pattern
import { Atom } from "@effect-atom/atom"
class EventEmitter {
private listeners = new Set<(data: any) => void>()
on(fn: (data: any) => void) {
this.listeners.add(fn)
return () => this.listeners.delete(fn)
}
emit(data: any) {
this.listeners.forEach(fn => fn(data))
}
}
const emitter = new EventEmitter()
const eventAtom = Atom.make((get) => {
let latest = "no events"
const unsubscribe = emitter.on((data) => {
latest = data
get.setSelf(latest)
})
get.addFinalizer(unsubscribe)
return latest
})