DeepSeek Harness: 自動クリーンアップと ctx.effect()
最終更新:2026-08-31
Cordis の最も強力な設計の一つが自動クリーンアップ——プラグインがアンロードされると、ctx を通じて登録されたすべてのリソースが自動的に回収され、手動の解放が不要です。しかし、リソースが ctx で直接管理されていない場合、ctx.effect() が手動クリーンアップのエントリポイントになります。
📋 前提知識:11-first-plugin.md の完了、apply 関数と Context を理解していること
1. 学習内容
- 自動クリーンアップの原理:ctx を通じて登録されたすべてのリソースが自動回収
ctx.effect():手動リソースクリーンアップの登録- クリーンアップ関数返却パターン
- setInterval/setTimeout の適切なクリーンアップ
- ネットワーク接続のクリーンアップパターン
- よくあるクリーンアップエラーと回避方法
2. 自動クリーンアップの原理
(1) ctx はリソース登録センター
各 ctx インスタンスはプラグインが登録したクリーンアップ可能なリソースを記録するレジストリを維持します:
class Context {
private _disposables: Disposable[] = []
register(disposable: Disposable) {
this._disposables.push(disposable)
}
dispose() {
for (const d of this._disposables.reverse()) {
d.dispose()
}
}
}
プラグインのアンロード時、ctx.dispose() が登録の逆順ですべてのリソースを回収します。
(2) 自動クリーンアップ可能なリソースタイプ
ctx を通じて登録された以下のリソースはすべて自動的にクリーンアップされます:
| 登録方法 | クリーンアップ動作 |
|---|---|
ctx.on('event', handler) |
イベントリスナーの削除 |
ctx.setInterval(fn, ms) |
タイマーのクリア |
ctx.setTimeout(fn, ms) |
タイマーのクリア |
ctx.command('name') |
コマンドの登録解除 |
ctx.service('name', impl) |
サービスの登録解除 |
▶ サンプル 3:
import { Context } from '@deepseek-ai/cordis'
export const name = 'auto-cleanup-demo'
export function apply(ctx: Context) {
// 以下の登録はすべて自動的にクリーンアップされる
ctx.on('session/created', (s) => {
ctx.logger.info(`session: ${s.id}`)
})
ctx.setInterval(() => {
ctx.logger.info('tick')
}, 10000)
ctx.command('demo')
.action(() => 'demo command')
}
// プラグインのアンロード時:リスナー削除 + タイマークリア + コマンド登録解除、手動コードゼロ
3. ctx.effect():手動リソースクリーンアップ
(1) 手動クリーンアップが必要な理由
すべてのリソースが ctx を通じて直接登録できるわけではありません。例えば:
- サードパーティライブラリが作成する接続(例:WebSocket、データベース接続プール)
- ネイティブ Node.js API が作成するリソース(例:
net.Server) - グローバル状態の変更(例:一時的な
process.env変数)
ここで ctx.effect() が登場します:
ctx.effect(() => {
// クリーンアップ関数を返す
return () => {
// クリーンアップロジック
}
})
▶ サンプル 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() はクリーンアップ関数を返すファクトリ関数を受け取ります。プラグインのアンロード時、Cordis がクリーンアップ関数を呼び出してリソースを解放します。
▶ サンプル 3:
// スタイル1:クリーンアップ関数を返す(推奨)
ctx.effect(() => {
const ws = new WebSocket('ws://localhost:8080')
return () => ws.close()
})
// スタイル2:クリーンアップ関数の参照を渡す
const cleanup = () => { /* ... */ }
ctx.effect(cleanup)
スタイル1の利点は、リソースの作成とクリーンアップが同じクロージャ内にあり、ロジックの凝集度が高いことです。
4. クリーンアップ関数返却パターン
(1) 標準パターン
ctx.effect(() => {
const resource = acquireResource()
return () => {
releaseResource(resource)
}
})
この「取得-解放」パターンは try-finally に似ています:
// 等価な try-finally メンタルモデル
try {
const resource = acquireResource()
// リソースを使用
} finally {
releaseResource(resource)
}
(2) 複数リソースのクリーンアップ
export function apply(ctx: Context) {
ctx.effect(() => {
const db = openDatabase()
const cache = openCache()
return () => {
cache.close() // 依存するものを先に閉じる
db.close() // 次に依存元を閉じる
}
})
}
⚠️ クリーンアップの順序は重要——他のリソースに依存するオブジェクトを先に閉じ、次にそれが依存するリソースを閉じます。
(3) クリーンアップ関数内のエラー処理
ctx.effect(() => {
const conn = createConnection()
return () => {
try {
conn.close()
} catch (e) {
ctx.logger.warn('cleanup error:', e)
}
}
})
クリーンアップ関数内の例外は他のリソースのクリーンアップを中断すべきではありません。Cordis は内部的に各クリーンアップ関数に try-catch 保護を持っていますが、明示的な処理がより安全です。
5. setInterval/setTimeout の適切なクリーンアップ
(1) 自動クリーンアップ方法(推奨)
export function apply(ctx: Context) {
// ctx.setInterval を使用 — 自動クリーンアップ
ctx.setInterval(() => {
ctx.logger.info('heartbeat')
}, 30000)
}
(2) ネイティブ API + ctx.effect()
ネイティブの setInterval を使用しなければならない場合:
export function apply(ctx: Context) {
const timer = setInterval(() => {
ctx.logger.info('heartbeat')
}, 30000)
ctx.effect(() => {
return () => clearInterval(timer)
})
}
(3) 比較
| 方法 | コード量 | 信頼性 | 推奨 |
|---|---|---|---|
ctx.setInterval |
1行 | 高い(自動) | ✅ |
ネイティブ + ctx.effect() |
3行 | 中程度(手動) | ⚠️ |
| ネイティブ(クリーンアップなし) | 1行 | 低い(リーク) | ❌ |
(4) setTimeout の落とし穴
// ❌ 間違い:アンロード後も setTimeout が発火
export function apply(ctx: Context) {
setTimeout(() => {
ctx.logger.info('delayed action') // プラグインは既にアンロードされている可能性!
}, 5000)
}
// ✅ 正しい:ctx.setTimeout を使用
export function apply(ctx: Context) {
ctx.setTimeout(() => {
ctx.logger.info('delayed action') // アンロード後は発火しない
}, 5000)
}
6. ネットワーク接続のクリーンアップパターン
(1) HTTP サーバー
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 接続
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) データベース接続プール
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')
}
})
}
⚠️ クリーンアップ関数は async にできます。Cordis は async クリーンアップの完了を待機してから次のクリーンアップを継続します。
(4) イベントリスナーのクリーンアップ
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. よくあるクリーンアップエラー
(1) クリーンアップの登録忘れ
// ❌ リーク:アンロード後もタイマーが実行
export function apply(ctx: Context) {
setInterval(() => {
console.log('orphan timer')
}, 1000)
}
修正:ctx.setInterval を使用するか ctx.effect() を登録。
(2) クリーンアップの順序エラー
// ❌ データベースを先に閉じ、それに依存するキャッシュを後で閉じる
ctx.effect(() => {
const db = openDB()
const cache = new Cache(db)
return () => {
db.close() // db を先に閉じた
cache.close() // キャッシュが内部的に db にアクセス → エラー
}
})
修正:クリーンアップの順序を逆にする。
(3) クリーンアップ内の未キャッチ例外
// ❌ クリーンアップ関数がスローし、後続のクリーンアップを中断
ctx.effect(() => {
return () => {
throw new Error('cleanup failed') // 他の effect が実行されない可能性
}
})
修正:クリーンアップロジックを try-catch でラップ。
(4) 古いクロージャ参照
// ❌ アンロード後に無効になる可能性のある外部変数を参照
let globalRef: SomeObject | null = new SomeObject()
export function apply(ctx: Context) {
ctx.effect(() => {
return () => {
globalRef!.cleanup() // globalRef が他のコードで null に設定されている可能性
}
})
}
修正:effect クロージャ内で参照をキャプチャ。
❓ よくある質問
ctx.on() はアンロード時に自動削除されるイベントリスナーを登録します。ctx.effect() はアンロード時に呼び出される任意のクリーンアップ関数を登録します。互いに補完:ctx.on はイベントを、ctx.effect はその他のリソースを処理します。📖 まとめ
- 自動クリーンアップは Cordis のコア機能:ctx を通じて登録されたリソースはアンロード時に自動回収
ctx.effect()は手動リソースクリーンアップを登録し、クリーンアップ関数を返す- クリーンアップ関数は登録の逆順(LIFO)で実行;依存順序に注意
- ネイティブ API より
ctx.setInterval/setTimeoutを優先 - ネットワーク接続、データベース接続プール等は
ctx.effect()でクリーンアップ登録が必要 - クリーンアップ関数を try-catch でラップし、例外が後続のクリーンアップを中断しないよう防止
📝 練習問題
1. ⭐ 基礎:ctx.setInterval を使って毎秒カウントを出力するプラグインを書いてください。起動し、アンロード時にタイマーが適切にクリーンアップされることを確認すること。
2. ⭐⭐ 応用:ポート3456でリッスンする HTTP サーバーを作成し、ctx.effect() でクリーンアップを登録するプラグインを書いてください。起動し、HTTP リクエストをテストし、プラグインをアンロードしてポートが解放されることを確認すること。
3. ⭐⭐⭐ チャレンジ:WebSocket 接続とデータベース接続プールの両方を管理するプラグインを書き、クリーンアップで最初に WebSocket を閉じ、次にデータベースを閉じることを確認し、クリーンアップ関数に例外処理を含めること。テスト:意図的にデータベースのクローズ中にエラーをスローし、WebSocket が依然として適切に閉じられることを確認すること。