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.

💡 Tip: Auto-cleanup is Cordis's safety net; ctx.effect() is your supplement. The principle: use ctx registration whenever possible; only use ctx.effect() when you can't.

📋 Prerequisites: Completed 11-first-plugin.md, understand the apply function and Context

1. What You'll Learn


Effect Cleanup

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:

TYPESCRIPT
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

TYPESCRIPT
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:

This is where ctx.effect() comes in:

TYPESCRIPT
ctx.effect(() => {
  // Return a cleanup function
  return () => {
    // Cleanup logic
  }
})

(2) ▶ Example 2

TYPESCRIPT
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

TYPESCRIPT
// 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

TYPESCRIPT
ctx.effect(() => {
  const resource = acquireResource()
  
  return () => {
    releaseResource(resource)
  }
})

This "acquire-release" pattern is similar to try-finally:

TYPESCRIPT
// Equivalent try-finally mental model
try {
  const resource = acquireResource()
  // Use resource
} finally {
  releaseResource(resource)
}

(2) Cleaning Up Multiple Resources

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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:

TYPESCRIPT
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

TYPESCRIPT
// ❌ Wrong: setTimeout still fires after unload
export function apply(ctx: Context) {
  setTimeout(() => {
    ctx.logger.info('delayed action') // Plugin may already be unloaded!
  }, 5000)
}
TYPESCRIPT
// ✅ 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

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
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

TYPESCRIPT
// ❌ 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

TYPESCRIPT
// ❌ 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

TYPESCRIPT
// ❌ 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

TYPESCRIPT
// ❌ 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

Q Can ctx.effect() cleanup functions be async?
A Yes. Cordis supports async cleanup functions and will await completion before continuing. Note: if cleanup takes too long, it will delay the entire plugin's unload.
Q What's the execution order of multiple ctx.effect() calls?
A Reverse registration order (LIFO, like a stack). Later-registered effects clean up first, ensuring correct dependency ordering.
Q Will resources be cleaned up if the plugin crashes?
A Yes. Cordis still attempts to call all registered cleanup functions when a plugin exits abnormally, including those registered via ctx.effect(). This is Cordis's safety guarantee.
Q Can I register ctx.effect() outside of apply?
A Technically yes (as long as you hold a ctx reference), but it's not recommended. Effects registered outside apply don't belong to any plugin lifecycle and may cause unpredictable cleanup behavior.
Q What's the difference between ctx.on() and ctx.effect()?
A 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


📝 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.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏