How Fiber Effects Handle Asynchronous Resource Management in Cordis
Yes, Cordis's Fiber class treats effects as first-class primitives that fully support asynchronous resource acquisition and cleanup, handling Promises, AsyncIterables, and synchronous disposables through a unified disposal pipeline.
Cordis is a modern plugin framework built around Fiber-based lifecycle management. The Fiber component in packages/core/src/fiber.ts provides a robust mechanism for handling resources that require asynchronous initialization or teardown, such as database connections, network servers, or external processes. Understanding how Fiber effects handle asynchronous resource management enables developers to build reliable plugins with guaranteed cleanup semantics.
The Effect Type System: Supporting Async Primitives
Cordis defines a flexible type hierarchy that accommodates both synchronous and asynchronous resource patterns.
Union Types for Sync and Async Effects
In packages/core/src/fiber.ts (lines 54-65), the Effect type is defined as a discriminated union:
type Effect<T = any> =
| SyncEffect<T> // () => Disposable<T> | Iterable<Disposable<T>>
| AsyncEffect<T> // () => Promise<Disposable<T>> | AsyncIterable<Disposable<T>>
An AsyncEffect can return a Promise<Disposable<T>> for single async resources or an AsyncIterable<Disposable<T>> for streaming multiple resources over time. This design allows a single Fiber.effect call to handle both immediate async acquisition and batched resource creation.
Execution Flow for Asynchronous Effects
When Fiber.effect is invoked, the framework routes the operation through the _execute method to handle resolution and collection of disposables.
Promise Resolution in _execute
The _execute implementation (lines 29-72) detects thenable objects by checking for a then property. If the effect returns a Promise, the fiber awaits resolution and stores the resulting disposable. Specific handling for promise-based effects appears at lines 90-95, where the resolved value is pushed onto the internal _disposables stack.
AsyncIterable Handling
For effects returning async generators, the execution path at lines 56-68 iterates using Symbol.asyncIterator. Each yielded disposable is collected asynchronously, allowing resources to be constructed and registered incrementally. This enables patterns like connection pooling where each worker must be initialized and cleaned up independently.
Guaranteed Cleanup with DisposableList
Asynchronous cleanup requires careful sequencing to prevent resource leaks during hot-reloads or shutdowns.
The _unload Sequence
The _unload method (lines 37-48) iterates through the DisposableList in reverse registration order (LIFO), awaiting any promise returned by a disposer before proceeding to the next. This sequential awaiting ensures that dependent resources teardown in the correct order. The disposables stack is maintained at lines 12-14, providing O(1) push and pop operations for resource tracking.
State Management and Error Propagation
Cordis tracks asynchronous operation state to prevent race conditions during load and unload cycles.
Tracking Async Operations with Inertia
The _setEpoch method (lines 99-115) manages the inertia promise, which represents the current load or unload operation. During async effect execution, the fiber enters FiberState.LOADING or FiberState.UNLOADING states. Developers can await fiber.await() to guarantee that all async effects have settled before proceeding, ensuring deterministic state transitions.
Error Handling in Async Flows
If an async effect throws or a disposable rejects, the error is wrapped via composeError and propagated to ctx.logger.error (lines 21-28). The fiber transitions to FiberState.FAILED, and subsequent calls to await re-throw the error, allowing explicit error handling in plugin code while preventing unhandled promise rejections.
Practical Code Examples
Async Database Connection
Acquire a database client asynchronously and ensure connection closure during teardown:
export default function (ctx: Context) {
ctx.effect(async () => {
const client = await createDbClient(); // async acquisition
return async () => {
await client.close(); // async clean-up
};
}, 'db-client');
}
The effect returns a Promise<Disposable>; Fiber.effect stores the resolved disposer and executes it during the unload cycle (see lines 90-95).
Streaming Disposables with Async Generators
Manage a pool of workers where each initialization is asynchronous:
export default function (ctx: Context) {
ctx.effect(async function* () {
for (let i = 0; i < 3; i++) {
const w = await spawnWorker(i);
yield async () => {
await w.terminate(); // async cleanup per worker
};
}
}, 'worker-pool');
}
Because the effect returns an AsyncIterable<Disposable>, the fiber iterates at lines 56-68, collecting each disposer and awaiting them sequentially on unload.
Coordinating Mixed Sync and Async Resources
Combine different resource types while maintaining disposal order:
export default function (ctx: Context) {
ctx.effect(() => {
const server = http.createServer(app);
server.listen(0);
return async () => {
await new Promise<void>((resolve) => server.close(() => resolve()));
};
}, 'http-server');
ctx.effect(async () => {
const cache = await initCache(); // async init
return () => cache.shutdown(); // sync disposer
}, 'cache');
}
The fiber tracks disposables in the _disposables stack (lines 12-14) and guarantees correct LIFO ordering, even with mixed sync and async disposers.
Summary
- Effect types in Cordis accept
Promise<Disposable>andAsyncIterable<Disposable>via the union type defined at lines 54-65 offiber.ts. - Execution handling awaits Promises and iterates AsyncIterables in
_execute(lines 29-72), storing results in aDisposableList. - Cleanup guarantees are enforced by
_unload(lines 37-48), which sequentially awaits async disposers in reverse registration order. - State tracking uses
inertiapromises andFiberStateenums (lines 99-115) to coordinate concurrent load/unload operations. - Error propagation wraps async errors with
composeErrorand transitions the fiber toFiberState.FAILED(lines 21-28).
Frequently Asked Questions
Can Fiber effects return a plain Promise for resource cleanup?
Yes. Effects may return Promise<Disposable> where the disposable itself can be synchronous or asynchronous. According to the implementation at lines 90-95, the fiber awaits the initial Promise, then stores the resulting cleanup function. During unload, if that cleanup function returns a Promise, the fiber awaits it before proceeding to the next disposable.
How does Cordis handle errors during asynchronous cleanup?
Errors are caught in the _unload execution path, wrapped using composeError, and logged via ctx.logger.error. If an async disposer rejects, the fiber transitions to FiberState.FAILED and stores the error. The error is then re-thrown on the next call to fiber.await(), allowing plugins to handle cleanup failures explicitly while ensuring the unload sequence continues or aborts based on the error propagation logic in lines 21-28.
What happens if an async effect is still loading when the fiber unloads?
The inertia promise tracks the ongoing operation via _setEpoch (lines 99-115). If _unload is called while the fiber is in FiberState.LOADING, the unload operation chains off the existing promise, ensuring that partial initializations complete or fail before cleanup begins. This prevents race conditions where a resource might be disposed before it fully initializes.
Can I mix synchronous and asynchronous effects in the same plugin?
Yes. The Effect type union explicitly supports both SyncEffect and AsyncEffect patterns. The execution engine in _execute dynamically detects thenable objects versus immediate values, allowing synchronous iterables and async generators to coexist. The disposal stack at lines 12-14 handles both types uniformly, awaiting async disposers while invoking sync disposers immediately.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →