Lifecycle and Middleware
Every ctx.plugin() returns a Fiber, which is a finite state machine. All registrations (ctx.on / ctx.effect / ctx.provide) are effects, recorded into the fiber's _disposables list and run in reverse order on unload. The event system's emit / parallel / serial / bail / waterfall dispatches cover all communication patterns: notification / aggregation / serial / short-circuit / middleware.
The Fiber State Machine
vendor/cordis/src/fiber.ts:147-154 defines const enum FiberState:PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↓ (failure)
FAILEDSix states: PENDING / LOADING / ACTIVE / FAILED / DISPOSED / UNLOADING, each with clear semantics.
Fiber Fields
The class Fiber fields at vendor/cordis/src/fiber.ts:184-194: uid / ctx / config / state / dispose / store / inject / runtime, with _disposables being a DisposableList.
Fiber Constructor
vendor/cordis/src/fiber.ts:222-263:- Creates the child ctx.
- Injects the intercept config.
- Sets up
_runner.execute(newfor classes, direct call for functions). - Registers
fiber.disposeas an effect of the parent.
Effect: Reversible Registration
effect(execute, label) at vendor/cordis/src/fiber.ts:415-560 supports five Effect shapes:
- sync — synchronous function
- async — async function
- promise — a Promise
- Iterable — iterable (each
next()yields a disposer) - AsyncIterable — async iterable
Disposers fire once; the effectInertia WeakMap lets external code join an already-running cleanup.
Key point: Effect is synonymous with "reversible registration".
ctx.oninternally callsregister, which in turn callsthis.ctx.fiber.effect(...)to enqueue the listener and return a disposer — that is why "unload = cleanup" is automatic.
Reload and Unload
_reload
_reload() at vendor/cordis/src/fiber.ts:646-673:
- Waits a frame so the old epoch is invalidated.
_resolveConfig(runs theinternal/configwaterfall)._execute(runner).- Clears
_error.
_unload
_unload() at vendor/cordis/src/fiber.ts:675-696:
Promise.all(this._disposables.clear().map(...))Runs disposers concurrently (reverse order is handled inside DisposableList), swallows errors and logs them.
Disposer ordering: inside a single effect, reverse order; across multiple async disposers, concurrent. If strict serial execution is required, await inside a single disposer.
update (the HMR mount point)
update(config, noSave) at vendor/cordis/src/fiber.ts:736-753:
update(config, noSave) {
ctx.waterfall(this, 'internal/update', config, noSave, () => restart())
}The internal/update waterfall is the mount point for HMR and Loader persistence: the loader writes back entry.options.config = config and calls tree.write() here; HMR decides here whether to restart.
Event System: Five Dispatches
interface Events at vendor/cordis/src/events.ts:329-352 has eight built-in framework events:
internal/plugin | internal/status | internal/config | internal/update
internal/get | internal/set | internal/listener | internal/dispatchThe doc mode is annotated with @mode.
dispatch and filter
dispatch(type, args) at vendor/cordis/src/events.ts:165-175: strips thisArg / name, fires internal/dispatch, filters listeners by Context.filter.
waterfall: the Middleware Model
waterfall(...args) at vendor/cordis/src/events.ts:234-243:
waterfall(...args) {
const next = args.pop()
// outer listener runs first, next() chains the delegation
// not calling next() means veto
}This is the classic onion-model middleware — the outer layer runs first, delegates inward via next(), and not calling next() is a veto.
Framework's Own Event Priority
When EventsService is constructed it auto-registers an internal/listener bail listener that re-routes listeners for internal/update into a separate fiber._hooks store — this is how "the framework's own events get priority", preventing plugin listeners from interfering with internal state changes.
What to read next
- Dependency Resolution and Ordering — how service availability drives fiber state
- Unloading and Hot Reload — how HMR hooks onto internal/update