DeepSeek Harness: Auto-Cleanup and ctx.effect()
Last updated: 2026-08-31
One of Cordis's most powerful designs is auto-cleanup — when a plugin unloads, all resources registered through ctx are automatically reclaimed with no manual release needed. But when resources aren't directly managed by ctx, ctx.effect() is your entry point for manual cleanup.
📋 Prerequisites: Completed 11-first-plugin.md, understand the apply function and Context
1. What You'll Learn
- Auto-cleanup principle: all resources registered through ctx are automatically reclaimed
ctx.effect(): manual resource cleanup registration- Return cleanup function pattern
- Proper cleanup of setInterval/setTimeout
- Network connection cleanup patterns
- Common cleanup errors and how to avoid them
2. Auto-Cleanup Principle
(1) ctx Is the Resource Registration Center
Each ctx instance maintains a registry recording all cleanable resources registered by the plugin:
class Context {
private _disposables: Disposable[] = []
register(disposable: Disposable) {
this._disposables.push(disposable)
}
dispose() {
for (const d of this._disposables.reverse()) {
d.dispose()
}
}
}
When the plugin unloads, ctx.dispose() reclaims all resources in reverse registration order.
(2) Auto-Cleanable Resource Types
The following resources registered through ctx are all automatically cleaned up:
| Registration Method | Cleanup Behavior |
|---|---|
ctx.on('event', handler) |
Remove event listener |
ctx.setInterval(fn, ms) |
Clear timer |
ctx.setTimeout(fn, ms) |
Clear timer |
ctx.command('name') |
Unregister command |
ctx.service('name', impl) |
Unregister service |
(3) ▶ Example 3
import { Context } from '@deepseek-ai/cordis'
export const name = 'auto-cleanup-demo'
export function apply(ctx: Context) {
// All registrations below are automatically cleaned up
ctx.on('session/created', (s) => {
ctx.logger.info(`session: ${s.id}`)
})
ctx.setInterval(() => {
ctx.logger.info('tick')
}, 10000)
ctx.command('demo')
.action(() => 'demo command')
}
// On plugin unload: listener removed + timer cleared + command unregistered, zero manual code
3. ctx.effect(): Manual Resource Cleanup
(1) Why Manual Cleanup Is Needed
Not all resources can be registered directly through ctx. For example:
- Connections created by third-party libraries (e.g., WebSocket, database connection pools)
- Resources created by native Node.js APIs (e.g.,
net.Server) - Global state modifications (e.g., temporary
process.envvariables)
This is where ctx.effect() comes in:
ctx.effect(() => {
// Return a cleanup function
return () => {
// Cleanup logic
}
})
(2) ▶ Example 2
import { Context } from '@deepseek-ai/cordis'
export const name = 'manual-cleanup'
export function apply(ctx: Context) {
const connection = createExternalConnection()
ctx.effect(() => {
return () => {
connection.close()
ctx.logger.info('connection closed')
}
})
}
ctx.effect() receives a factory function that returns a cleanup function. When the plugin unloads, Cordis calls the cleanup function to release resources.
(3) ▶ Example 3
// Style 1: Return cleanup function (recommended)
ctx.effect(() => {
const ws = new WebSocket('ws://localhost:8080')
return () => ws.close()
})
// Style 2: Pass cleanup function reference
const cleanup = () => { /* ... */ }
ctx.effect(cleanup)
Style 1's advantage is that resource creation and cleanup are in the same closure, keeping logic cohesive.
4. Return Cleanup Function Pattern
(1) Standard Pattern
ctx.effect(() => {
const resource = acquireResource()
return () => {
releaseResource(resource)
}
})
This "acquire-release" pattern is similar to try-finally:
// Equivalent try-finally mental model
try {
const resource = acquireResource()
// Use resource
} finally {
releaseResource(resource)
}
(2) Cleaning Up Multiple Resources
export function apply(ctx: Context) {
ctx.effect(() => {
const db = openDatabase()
const cache = openCache()
return () => {
cache.close() // Close dependent first
db.close() // Then close the dependency
}
})
}
⚠️ Cleanup order matters — close objects that depend on other resources first, then close the resources they depend on.
(3) Error Handling in Cleanup Functions
ctx.effect(() => {
const conn = createConnection()
return () => {
try {
conn.close()
} catch (e) {
ctx.logger.warn('cleanup error:', e)
}
}
})
Exceptions in cleanup functions should not interrupt the cleanup of other resources. Cordis internally has try-catch protection for each cleanup function, but explicit handling is safer.
5. Proper setInterval/setTimeout Cleanup
(1) Auto-Cleanup Method (Recommended)
export function apply(ctx: Context) {
// Use ctx.setInterval — auto-cleanup
ctx.setInterval(() => {
ctx.logger.info('heartbeat')
}, 30000)
}
(2) Native API + ctx.effect()
If you must use the native setInterval:
export function apply(ctx: Context) {
const timer = setInterval(() => {
ctx.logger.info('heartbeat')
}, 30000)
ctx.effect(() => {
return () => clearInterval(timer)
})
}
(3) Comparison
| Method | Code Amount | Reliability | Recommended |
|---|---|---|---|
ctx.setInterval |
1 line | High (automatic) | ✅ |
Native + ctx.effect() |
3 lines | Medium (manual) | ⚠️ |
| Native (no cleanup) | 1 line | Low (leak) | ❌ |
(4) setTimeout Pitfall
// ❌ Wrong: setTimeout still fires after unload
export function apply(ctx: Context) {
setTimeout(() => {
ctx.logger.info('delayed action') // Plugin may already be unloaded!
}, 5000)
}
// ✅ Correct: use ctx.setTimeout
export function apply(ctx: Context) {
ctx.setTimeout(() => {
ctx.logger.info('delayed action') // Won't fire after unload
}, 5000)
}
6. Network Connection Cleanup Patterns
(1) HTTP Server
import { createServer } from 'http'
export function apply(ctx: Context) {
const server = createServer((req, res) => {
res.end('ok')
})
server.listen(3456)
ctx.effect(() => {
return () => {
server.close()
ctx.logger.info('HTTP server closed')
}
})
}
(2) WebSocket Connection
import WebSocket from 'ws'
export function apply(ctx: Context) {
const ws = new WebSocket('ws://localhost:8080')
ws.on('open', () => {
ctx.logger.info('ws connected')
})
ctx.effect(() => {
return () => {
if (ws.readyState === WebSocket.OPEN) {
ws.close()
}
}
})
}
(3) Database Connection Pool
import { Pool } from 'pg'
export function apply(ctx: Context) {
const pool = new Pool({
connectionString: 'postgresql://localhost/mydb',
max: 10
})
ctx.effect(() => {
return async () => {
await pool.end()
ctx.logger.info('db pool closed')
}
})
}
⚠️ Cleanup functions can be async. Cordis will await async cleanup completion before continuing with subsequent cleanup.
(4) Event Listener Cleanup
export function apply(ctx: Context) {
const emitter = getExternalEmitter()
const handler = (data: any) => {
ctx.logger.info('event:', data)
}
emitter.on('data', handler)
ctx.effect(() => {
return () => {
emitter.off('data', handler)
}
})
}
7. Common Cleanup Errors
(1) Forgetting to Register Cleanup
// ❌ Leak: timer still runs after unload
export function apply(ctx: Context) {
setInterval(() => {
console.log('orphan timer')
}, 1000)
}
Fix: Use ctx.setInterval or register ctx.effect().
(2) Wrong Cleanup Order
// ❌ Close database first, then close cache that depends on it
ctx.effect(() => {
const db = openDB()
const cache = new Cache(db)
return () => {
db.close() // Closed db first
cache.close() // Cache internally accesses db → error
}
})
Fix: Reverse the cleanup order.
(3) Uncaught Exception in Cleanup
// ❌ Cleanup function throws, interrupting subsequent cleanup
ctx.effect(() => {
return () => {
throw new Error('cleanup failed') // Other effects may not execute
}
})
Fix: Wrap cleanup logic in try-catch.
(4) Stale Closure Reference
// ❌ References external variable that may be invalid after unload
let globalRef: SomeObject | null = new SomeObject()
export function apply(ctx: Context) {
ctx.effect(() => {
return () => {
globalRef!.cleanup() // globalRef may have been set to null by other code
}
})
}
Fix: Capture the reference inside the effect closure.
❓ FAQ
ctx.on() registers an event listener that's automatically removed on unload. ctx.effect() registers an arbitrary cleanup function called on unload. They complement each other: ctx.on handles events, ctx.effect handles other resources.📖 Summary
- Auto-cleanup is Cordis's core feature: resources registered through ctx are automatically reclaimed on unload
ctx.effect()registers manual resource cleanup, returning a cleanup function- Cleanup functions execute in reverse registration order (LIFO); be mindful of dependency order
- Prefer
ctx.setInterval/setTimeoutover native APIs - Network connections, database connection pools, etc. must use
ctx.effect()for cleanup registration - Wrap cleanup functions in try-catch to prevent exceptions from interrupting subsequent cleanup
📝 Exercises
1. ⭐ Basic: Write a plugin that outputs a count every second using ctx.setInterval. Start it and confirm the timer is properly cleaned up on unload.
2. ⭐⭐ Intermediate: Write a plugin that creates an HTTP server listening on port 3456, with cleanup registered via ctx.effect(). Start it, test HTTP requests, then unload the plugin and confirm the port is released.
3. ⭐⭐⭐ Challenge: Write a plugin that manages both a WebSocket connection and a database connection pool, ensuring cleanup closes the WebSocket first then the database, with exception handling in cleanup functions. Test: deliberately throw an error during database close, and verify the WebSocket is still properly closed.