DeepSeek Harness: 自動クリーンアップと ctx.effect()

最終更新:2026-08-31

Cordis の最も強力な設計の一つが自動クリーンアップ——プラグインがアンロードされると、ctx を通じて登録されたすべてのリソースが自動的に回収され、手動の解放が不要です。しかし、リソースが ctx で直接管理されていない場合、ctx.effect() が手動クリーンアップのエントリポイントになります。

💡 ヒント:自動クリーンアップは Cordis のセーフティネット;ctx.effect() は補完です。原則:可能な限り ctx 登録を使用;使えない場合のみ ctx.effect() を使用。

📋 前提知識11-first-plugin.md の完了、apply 関数と Context を理解していること

1. 学習内容

Effect クリーンアップ


2. 自動クリーンアップの原理

(1) ctx はリソース登録センター

ctx インスタンスはプラグインが登録したクリーンアップ可能なリソースを記録するレジストリを維持します:

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

TYPESCRIPT
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 を通じて直接登録できるわけではありません。例えば:

ここで ctx.effect() が登場します:

TYPESCRIPT
ctx.effect(() => {
  // クリーンアップ関数を返す
  return () => {
    // クリーンアップロジック
  }
})

▶ サンプル 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() はクリーンアップ関数を返すファクトリ関数を受け取ります。プラグインのアンロード時、Cordis がクリーンアップ関数を呼び出してリソースを解放します。

▶ サンプル 3:

TYPESCRIPT
// スタイル1:クリーンアップ関数を返す(推奨)
ctx.effect(() => {
  const ws = new WebSocket('ws://localhost:8080')
  return () => ws.close()
})

// スタイル2:クリーンアップ関数の参照を渡す
const cleanup = () => { /* ... */ }
ctx.effect(cleanup)

スタイル1の利点は、リソースの作成とクリーンアップが同じクロージャ内にあり、ロジックの凝集度が高いことです。


4. クリーンアップ関数返却パターン

(1) 標準パターン

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

この「取得-解放」パターンは try-finally に似ています:

TYPESCRIPT
// 等価な try-finally メンタルモデル
try {
  const resource = acquireResource()
  // リソースを使用
} finally {
  releaseResource(resource)
}

(2) 複数リソースのクリーンアップ

TYPESCRIPT
export function apply(ctx: Context) {
  ctx.effect(() => {
    const db = openDatabase()
    const cache = openCache()
    
    return () => {
      cache.close()  // 依存するものを先に閉じる
      db.close()     // 次に依存元を閉じる
    }
  })
}

⚠️ クリーンアップの順序は重要——他のリソースに依存するオブジェクトを先に閉じ、次にそれが依存するリソースを閉じます。

(3) クリーンアップ関数内のエラー処理

TYPESCRIPT
ctx.effect(() => {
  const conn = createConnection()
  return () => {
    try {
      conn.close()
    } catch (e) {
      ctx.logger.warn('cleanup error:', e)
    }
  }
})

クリーンアップ関数内の例外は他のリソースのクリーンアップを中断すべきではありません。Cordis は内部的に各クリーンアップ関数に try-catch 保護を持っていますが、明示的な処理がより安全です。


5. setInterval/setTimeout の適切なクリーンアップ

(1) 自動クリーンアップ方法(推奨)

TYPESCRIPT
export function apply(ctx: Context) {
  // ctx.setInterval を使用 — 自動クリーンアップ
  ctx.setInterval(() => {
    ctx.logger.info('heartbeat')
  }, 30000)
}

(2) ネイティブ API + ctx.effect()

ネイティブの setInterval を使用しなければならない場合:

TYPESCRIPT
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 の落とし穴

TYPESCRIPT
// ❌ 間違い:アンロード後も setTimeout が発火
export function apply(ctx: Context) {
  setTimeout(() => {
    ctx.logger.info('delayed action') // プラグインは既にアンロードされている可能性!
  }, 5000)
}
TYPESCRIPT
// ✅ 正しい:ctx.setTimeout を使用
export function apply(ctx: Context) {
  ctx.setTimeout(() => {
    ctx.logger.info('delayed action') // アンロード後は発火しない
  }, 5000)
}

6. ネットワーク接続のクリーンアップパターン

(1) HTTP サーバー

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 接続

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) データベース接続プール

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')
    }
  })
}

⚠️ クリーンアップ関数は async にできます。Cordis は async クリーンアップの完了を待機してから次のクリーンアップを継続します。

(4) イベントリスナーのクリーンアップ

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. よくあるクリーンアップエラー

(1) クリーンアップの登録忘れ

TYPESCRIPT
// ❌ リーク:アンロード後もタイマーが実行
export function apply(ctx: Context) {
  setInterval(() => {
    console.log('orphan timer')
  }, 1000)
}

修正:ctx.setInterval を使用するか ctx.effect() を登録。

(2) クリーンアップの順序エラー

TYPESCRIPT
// ❌ データベースを先に閉じ、それに依存するキャッシュを後で閉じる
ctx.effect(() => {
  const db = openDB()
  const cache = new Cache(db)
  return () => {
    db.close()      // db を先に閉じた
    cache.close()   // キャッシュが内部的に db にアクセス → エラー
  }
})

修正:クリーンアップの順序を逆にする。

(3) クリーンアップ内の未キャッチ例外

TYPESCRIPT
// ❌ クリーンアップ関数がスローし、後続のクリーンアップを中断
ctx.effect(() => {
  return () => {
    throw new Error('cleanup failed')  // 他の effect が実行されない可能性
  }
})

修正:クリーンアップロジックを try-catch でラップ。

(4) 古いクロージャ参照

TYPESCRIPT
// ❌ アンロード後に無効になる可能性のある外部変数を参照
let globalRef: SomeObject | null = new SomeObject()

export function apply(ctx: Context) {
  ctx.effect(() => {
    return () => {
      globalRef!.cleanup()  // globalRef が他のコードで null に設定されている可能性
    }
  })
}

修正:effect クロージャ内で参照をキャプチャ。


❓ よくある質問

Q ctx.effect() のクリーンアップ関数は async にできますか?
A はい。Cordis は async クリーンアップ関数をサポートし、完了を待機してから継続します。注意:クリーンアップに時間がかかると、プラグイン全体のアンロードが遅延します。
Q 複数の ctx.effect() 呼び出しの実行順序は?
A 登録の逆順(LIFO、スタックのように)。後で登録された effect が先にクリーンアップされ、正しい依存順序を保証します。
Q プラグインがクラッシュした場合、リソースはクリーンアップされますか?
A はい。プラグインが異常終了しても、Cordis は ctx.effect() で登録されたものを含め、すべての登録済みクリーンアップ関数の呼び出しを試みます。これが Cordis の安全性保証です。
Q apply の外で ctx.effect() を登録できますか?
A 技術的には可能(ctx 参照を保持していれば)が、推奨されません。apply の外で登録された effect はどのプラグインライフサイクルにも属さず、予測不能なクリーンアップ動作を引き起こす可能性があります。
Q ctx.on() と ctx.effect() の違いは?
A ctx.on() はアンロード時に自動削除されるイベントリスナーを登録します。ctx.effect() はアンロード時に呼び出される任意のクリーンアップ関数を登録します。互いに補完:ctx.on はイベントを、ctx.effect はその他のリソースを処理します。

📖 まとめ


📝 練習問題

1. ⭐ 基礎ctx.setInterval を使って毎秒カウントを出力するプラグインを書いてください。起動し、アンロード時にタイマーが適切にクリーンアップされることを確認すること。

2. ⭐⭐ 応用:ポート3456でリッスンする HTTP サーバーを作成し、ctx.effect() でクリーンアップを登録するプラグインを書いてください。起動し、HTTP リクエストをテストし、プラグインをアンロードしてポートが解放されることを確認すること。

3. ⭐⭐⭐ チャレンジ:WebSocket 接続とデータベース接続プールの両方を管理するプラグインを書き、クリーンアップで最初に WebSocket を閉じ、次にデータベースを閉じることを確認し、クリーンアップ関数に例外処理を含めること。テスト:意図的にデータベースのクローズ中にエラーをスローし、WebSocket が依然として適切に閉じられることを確認すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%