docs: add hands-on Cordis tutorial
Seven-chapter tutorial under docs/cordis-tutorial/ for agent developers new to Cordis: first plugin, lifecycle/effects, services, events, config, composition/HMR, and a final chapter registering a tool against real harness services. Every transcript was produced by running the chapter files in a gitignored tmp/ scratch directory. Published to both website locales as mirrored English pages under a new 'Cordis tutorial' develop-sidebar section; a Chinese pair can be added later without route changes.
This commit is contained in:
89
docs/cordis-tutorial/02-lifecycle-and-effects.md
Normal file
89
docs/cordis-tutorial/02-lifecycle-and-effects.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 2. Lifecycle and effects
|
||||
|
||||
A Cordis plugin can be unloaded by a config edit, hot reload, explicit disposal, or loss of a required service. Registrations made through Cordis APIs are effects and are undone when their owning plugin unloads; resources managed outside those APIs must be wrapped in `ctx.effect()`.
|
||||
|
||||
## Effects
|
||||
|
||||
For a resource Cordis does not already manage — a timer, a connection, a watcher — wrap it in `ctx.effect()` and return a disposer:
|
||||
|
||||
Create `lifecycle.ts` in `tmp/cordis-tutorial`:
|
||||
|
||||
```ts
|
||||
import type { Context } from 'cordis'
|
||||
|
||||
export const name = 'lifecycle-demo'
|
||||
|
||||
function heartbeat(ctx: Context) {
|
||||
console.log('heartbeat plugin loading')
|
||||
ctx.effect(() => {
|
||||
const timer = setInterval(() => console.log('tick'), 200)
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
console.log('heartbeat cleaned up')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function apply(ctx: Context) {
|
||||
// Mount a child plugin and keep its fiber to dispose it later.
|
||||
const fiber = ctx.plugin(heartbeat)
|
||||
setTimeout(async () => {
|
||||
await fiber.dispose()
|
||||
console.log('disposed')
|
||||
process.exit(0)
|
||||
}, 700)
|
||||
}
|
||||
```
|
||||
|
||||
Point `cordis.yml` at it:
|
||||
|
||||
```yaml
|
||||
- name: './lifecycle.ts'
|
||||
```
|
||||
|
||||
Run (`node --import tsx ../../vendor/cordis/bin.js`) and you get:
|
||||
|
||||
```
|
||||
heartbeat plugin loading
|
||||
tick
|
||||
tick
|
||||
tick
|
||||
heartbeat cleaned up
|
||||
disposed
|
||||
```
|
||||
|
||||
Three things to notice:
|
||||
|
||||
- `ctx.plugin(heartbeat)` mounts a plugin **from code** — the same operation the YAML loader performs for each config entry. It returns a **fiber**, the runtime handle for one loaded plugin instance.
|
||||
- The effect body runs during load; the disposer it returns runs during unload. You never call the disposer yourself for a plugin-lifetime resource.
|
||||
- `fiber.dispose()` resolves after all of the plugin's cleanup — including async disposers — has finished, and recursively unloads any child plugins it mounted.
|
||||
|
||||
## The fiber state machine
|
||||
|
||||
Every loaded plugin instance owns a fiber that moves through these states:
|
||||
|
||||
```
|
||||
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
|
||||
↘ FAILED
|
||||
```
|
||||
|
||||
- **PENDING** — declared, but a required service (chapter 3) is not available yet.
|
||||
- **LOADING / ACTIVE** — `apply` is running / has completed.
|
||||
- **FAILED** — `apply` or config validation threw.
|
||||
- **UNLOADING / DISPOSED** — disposers are running / everything is torn down.
|
||||
|
||||
You will meet PENDING again in [chapter 6](06-composition-and-hmr.md), where it is the usual answer to "why does my plugin print nothing?".
|
||||
|
||||
## What is already an effect
|
||||
|
||||
You rarely write `ctx.effect()` yourself, because the built-in registration APIs are effects already:
|
||||
|
||||
- `ctx.on(event, listener)` — the listener is removed on unload ([chapter 4](04-events.md)).
|
||||
- `ctx.plugin(child)` — the child is disposed with its parent.
|
||||
- Service registrations are effects. Harness registries such as `ctx.tools.register(...)` also attach their returned disposers to the calling plugin, so they unwind automatically ([chapter 7](07-into-the-harness.md)).
|
||||
|
||||
For a resource Cordis does not manage, acquire it inside `ctx.effect()` and return a disposer that releases it. Cordis then invokes that release during unloading, including hot reload.
|
||||
|
||||
One ordering caveat: disposers start in reverse registration order, but multiple **async** disposers run concurrently. If teardown steps must run in sequence, keep them in one disposer and await them there.
|
||||
|
||||
Next: [Services](03-services.md) — how plugins share capabilities.
|
||||
Reference in New Issue
Block a user