effect-resource-management
npx skills add https://github.com/thebushidocollective/han --skill effect-resource-management
Agent 安装分布
Skill 文档
Effect Resource Management
Master automatic resource management in Effect using Scopes and finalizers. This skill covers resource acquisition, cleanup, scoped effects, and patterns for building leak-free Effect applications.
Scope Fundamentals
A Scope represents the lifetime of resources. When a scope closes, all registered finalizers execute automatically.
Basic Scope Usage
import { Effect, Scope } from "effect"
const program = Effect.scoped(
Effect.gen(function* () {
// Resources acquired here are tied to this scope
const resource = yield* acquireResource()
// Use resource
const result = yield* useResource(resource)
return result
// Scope closes here, resources cleaned up automatically
})
)
Adding Finalizers
import { Effect } from "effect"
const acquireFile = (path: string) =>
Effect.gen(function* () {
// Acquire resource
const file = yield* Effect.sync(() => openFile(path))
// Register cleanup
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
console.log(`Closing file: ${path}`)
file.close()
})
)
return file
})
// Usage
const program = Effect.scoped(
Effect.gen(function* () {
const file = yield* acquireFile("data.txt")
const content = yield* readFile(file)
return content
// File automatically closed on scope exit
})
)
Finalizer Behavior
Execution Order
Finalizers execute in reverse order of registration (LIFO):
import { Effect } from "effect"
const program = Effect.scoped(
Effect.gen(function* () {
yield* Effect.addFinalizer(() =>
Effect.log("Finalizer 1")
)
yield* Effect.addFinalizer(() =>
Effect.log("Finalizer 2")
)
yield* Effect.addFinalizer(() =>
Effect.log("Finalizer 3")
)
return "done"
})
)
// Output:
// Finalizer 3
// Finalizer 2
// Finalizer 1
Exit Information
Finalizers receive exit information:
import { Effect, Exit } from "effect"
const acquireWithContext = Effect.gen(function* () {
yield* Effect.addFinalizer((exit) =>
Effect.sync(() => {
if (Exit.isSuccess(exit)) {
console.log("Scope exited successfully:", exit.value)
} else if (Exit.isFailure(exit)) {
console.log("Scope failed:", exit.cause)
} else {
console.log("Scope interrupted")
}
})
)
// Acquire resource
const resource = yield* Effect.sync(() => createResource())
return resource
})
Resource Patterns
Database Connection
import { Effect } from "effect"
interface DbConnection {
query: <T>(sql: string) => Promise<T>
close: () => Promise<void>
}
const acquireConnection = (config: DbConfig) =>
Effect.gen(function* () {
// Acquire connection
const conn = yield* Effect.tryPromise({
try: () => createConnection(config),
catch: (error) => ({
_tag: "ConnectionError",
message: String(error)
})
})
// Register cleanup
yield* Effect.addFinalizer(() =>
Effect.tryPromise({
try: () => conn.close(),
catch: (error) => ({
_tag: "CloseError",
message: String(error)
})
}).pipe(
Effect.catchAll((error) =>
Effect.log(`Failed to close connection: ${error.message}`)
)
)
)
return conn
})
// Usage
const queryDatabase = Effect.scoped(
Effect.gen(function* () {
const conn = yield* acquireConnection(dbConfig)
const users = yield* Effect.tryPromise(() =>
conn.query<User[]>("SELECT * FROM users")
)
return users
// Connection automatically closed
})
)
File Operations
import { Effect } from "effect"
import * as fs from "fs/promises"
const withFile = <A, E, R>(
path: string,
use: (handle: fs.FileHandle) => Effect.Effect<A, E, R>
) =>
Effect.scoped(
Effect.gen(function* () {
// Acquire file handle
const handle = yield* Effect.tryPromise({
try: () => fs.open(path, "r"),
catch: (error) => ({
_tag: "FileError",
message: String(error)
})
})
// Register cleanup
yield* Effect.addFinalizer(() =>
Effect.tryPromise(() => handle.close()).pipe(
Effect.catchAll(() => Effect.void)
)
)
// Use file
return yield* use(handle)
})
)
// Usage
const readFileContent = withFile("data.txt", (handle) =>
Effect.tryPromise(() => handle.readFile({ encoding: "utf8" }))
)
Network Resources
import { Effect } from "effect"
interface WebSocket {
send: (data: string) => void
close: () => void
onMessage: (handler: (data: string) => void) => void
}
const acquireWebSocket = (url: string) =>
Effect.gen(function* () {
const ws = yield* Effect.async<WebSocket, never>((resume) => {
const socket = new WebSocket(url)
socket.onopen = () => {
resume(Effect.succeed(socket))
}
socket.onerror = () => {
resume(Effect.fail({ _tag: "ConnectionError" }))
}
})
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
console.log("Closing WebSocket")
ws.close()
})
)
return ws
})
Scoped Effects
Effect.acquireRelease
Simplified resource acquisition:
import { Effect } from "effect"
const resource = Effect.acquireRelease(
// Acquire
Effect.sync(() => {
console.log("Acquiring resource")
return createResource()
}),
// Release
(resource) =>
Effect.sync(() => {
console.log("Releasing resource")
resource.cleanup()
})
)
// Usage
const program = Effect.scoped(
Effect.gen(function* () {
const r = yield* resource
return yield* useResource(r)
})
)
Effect.acquireUseRelease
One-shot resource usage:
import { Effect } from "effect"
const readConfig = Effect.acquireUseRelease(
// Acquire
Effect.tryPromise(() => fs.open("config.json", "r")),
// Use
(handle) =>
Effect.tryPromise(() =>
handle.readFile({ encoding: "utf8" })
).pipe(
Effect.map((content) => JSON.parse(content))
),
// Release
(handle) =>
Effect.tryPromise(() => handle.close()).pipe(
Effect.orDie
)
)
Nested Scopes
Scope Nesting
Scopes can be nested for hierarchical cleanup:
import { Effect } from "effect"
const program = Effect.scoped(
Effect.gen(function* () {
const db = yield* acquireConnection()
yield* Effect.scoped(
Effect.gen(function* () {
const transaction = yield* beginTransaction(db)
yield* updateUsers(transaction)
yield* commitTransaction(transaction)
// Transaction scope ends, resources cleaned up
})
)
// DB connection still alive
yield* runQuery(db)
// DB scope ends, connection closed
})
)
Parallel Scopes
import { Effect } from "effect"
const parallelResources = Effect.gen(function* () {
const results = yield* Effect.all([
Effect.scoped(
Effect.gen(function* () {
const conn1 = yield* acquireConnection(db1Config)
return yield* queryDb(conn1)
})
),
Effect.scoped(
Effect.gen(function* () {
const conn2 = yield* acquireConnection(db2Config)
return yield* queryDb(conn2)
})
)
])
return results
// Both connections closed automatically
})
Advanced Patterns
Resource Pool
import { Effect, Queue, Ref } from "effect"
interface Pool<R> {
acquire: Effect.Effect<R, never, Scope.Scope>
release: (resource: R) => Effect.Effect<void, never, never>
}
const createPool = <R, E>(
create: Effect.Effect<R, E, never>,
destroy: (resource: R) => Effect.Effect<void, never, never>,
size: number
): Effect.Effect<Pool<R>, E, Scope.Scope> =>
Effect.gen(function* () {
const available = yield* Queue.bounded<R>(size)
const counter = yield* Ref.make(0)
// Initialize pool
yield* Effect.forEach(
Array.from({ length: size }),
() =>
Effect.gen(function* () {
const resource = yield* create
yield* Queue.offer(available, resource)
}),
{ concurrency: "unbounded" }
)
// Register pool cleanup
yield* Effect.addFinalizer(() =>
Effect.gen(function* () {
const resources = yield* Queue.takeAll(available)
yield* Effect.forEach(
resources,
(r) => destroy(r),
{ concurrency: "unbounded" }
)
})
)
return {
acquire: Effect.gen(function* () {
const resource = yield* Queue.take(available)
yield* Effect.addFinalizer(() => Queue.offer(available, resource))
return resource
}),
release: (resource) => Queue.offer(available, resource)
}
})
Cached Resource
import { Effect, Ref } from "effect"
const cached = <A, E, R>(
acquire: Effect.Effect<A, E, R>
): Effect.Effect<Effect.Effect<A, E, never>, never, Scope.Scope | R> =>
Effect.gen(function* () {
const ref = yield* Ref.make<Option<A>>(Option.none())
yield* Effect.addFinalizer(() =>
ref.set(Option.none())
)
return ref.get.pipe(
Effect.flatMap((option) =>
Option.match(option, {
onNone: () =>
acquire.pipe(
Effect.tap((value) => ref.set(Option.some(value)))
),
onSome: (value) => Effect.succeed(value)
})
)
)
})
Best Practices
-
Always Use Scoped: Acquire resources within Effect.scoped.
-
Register Finalizers Immediately: Add finalizers right after acquisition.
-
Handle Cleanup Errors: Catch and log errors in finalizers.
-
Reverse Order: Rely on LIFO finalizer execution for dependencies.
-
Use acquireRelease: Prefer acquireRelease for simple acquire/release patterns.
-
Test Cleanup: Verify finalizers execute correctly.
-
Avoid Manual Cleanup: Don’t manually clean up scoped resources.
-
Nest Appropriately: Use nested scopes for hierarchical resources.
-
Pool Expensive Resources: Use resource pools for expensive acquisitions.
-
Document Scope Requirements: Make it clear which effects need scopes.
Common Pitfalls
-
Missing Scoped: Acquiring resources without Effect.scoped.
-
Not Adding Finalizers: Forgetting to register cleanup.
-
Finalizer Errors: Throwing errors in finalizers without handling.
-
Wrong Scope Nesting: Closing scopes in wrong order.
-
Resource Leaks: Not cleaning up on all exit paths.
-
Duplicate Cleanup: Cleaning up resources multiple times.
-
Blocking Finalizers: Using long-running operations in finalizers.
-
Ignoring Exit Info: Not using exit information appropriately.
-
Scope Scope Confusion: Confusing when scopes close.
-
Missing Error Handling: Not handling errors during acquisition.
When to Use This Skill
Use effect-resource-management when you need to:
- Manage database connections
- Handle file operations safely
- Work with network resources
- Implement connection pools
- Build transaction systems
- Ensure cleanup on all exit paths
- Manage WebSocket connections
- Handle distributed locks
- Implement caching with cleanup
- Build leak-free applications
Resources
Official Documentation
Related Skills
- effect-core-patterns – Basic Effect operations
- effect-concurrency – Managing fiber lifecycles
- effect-dependency-injection – Layer cleanup with scoped