cordis-rs
English | 简体中文
A runtime-agnostic Rust port of Cordis 4.x — the plugin framework at the core of DeepSeek Harness, vendored there as @deepseek-ai/cordis.
This implementation is based on Cordis 4.0.1 from DeepSeek Harness. Its core structure mirrors the original
Context / Events / Fiber / Logger / Reflect / Registry / Servicemodules and preserves automatic activation when dependencies arrive, automatic unloading when dependencies disappear, scoped isolation, effect cleanup, and all five event dispatch modes as closely as Rust allows.
Cordis is a context-based plugin framework for applications that need explicit dependency injection, scoped services, lifecycle-managed cleanup, structured events, and configuration-driven plugins. cordis-rs preserves that model while replacing JavaScript-only mechanisms (Proxy, prototype inheritance, callable objects, decorators, and any) with explicit Rust APIs, Arc, and checked downcasts.
Status
The crate currently ports the complete core runtime:
| TypeScript Cordis | Rust API | Status |
|---|---|---|
new Context() / extend() |
Context::new() / extend() |
✅ |
isolate() and shared labels |
isolate() / isolate_with() |
✅ |
intercept() |
intercept() / intercepts() |
✅ |
Proxy-backed get/set/provide |
typed get/require/set/provide |
✅ |
| Accessor and mixin reflection | accessor() and explicit alias() |
✅¹ |
| Function/object/class plugins | Plugin, plugin_sync, plugin_async, service adapters |
✅ |
inject dependency epochs |
Inject and automatic unload/reload |
✅ |
FiberState, wait, restart, update, dispose |
same lifecycle operations | ✅ |
| Sync/async/generator effects | sync/async disposers and nested effect handles | ✅² |
emit/parallel/serial/bail/waterfall |
same five dispatch modes | ✅ |
| Context listener filters | with_filter() / emit_from() |
✅ |
| Logger buffer/exporters/levels/formatters | corresponding logger APIs | ✅ |
| Standard Schema validation | Plugin::validate_config + validation issues |
✅³ |
internal/plugin, internal/status, internal/service, internal/dispatch |
same meta-events | ✅⁴ |
Intercept meta-events (internal/get/set/config/update/listener) |
not ported | Not included |
| Decorators and callable services | explicit Rust traits/builders | Rust-native |
| Loader/include/HMR packages | outside the core crate | Not included |
- Rust cannot dynamically project arbitrary struct fields like a JavaScript Proxy, so
alias()is the explicit counterpart to commonmixin()usage. - Rust plugin code registers multiple effects explicitly;
EffectHandle::adopt()provides the original nested diagnostic/disposal tree. - Validation is trait-based because Standard Schema is a JavaScript protocol.
internal/dispatchcarries(mode, name, args); the upstream fourththisArgargument is omitted. The waterfall/bail interception points used by upstream HMR and config injection (internal/get,internal/set,internal/config,internal/update,internal/listener) are not part of this port, so downstream code relying on them needs a different extension point.
Design goals
- Faithful lifecycle: a plugin remains
Pendinguntil every injected service is active. Replacing/removing a provider changes the dependency epoch, unloads the consumer, and starts it again when possible. - Scoped DI: isolated branches resolve different implementations of the same service. Reusing an
Isolationlabel joins scopes. - Ownership-based cleanup: plugins, listeners, services, exporters, accessors, and child plugins are effects of their creating fiber.
- No executor lock-in: the crate has no third-party dependencies. Futures are accepted through boxed standard-library futures; eager lifecycle operations use a small wake-aware executor.
- Type-checked dynamic values: service, config, and event storage uses
Value(Arc<dyn Any + Send + Sync>) with checked downcasting and useful type errors.
Install
[]
= "0.3"
The package is published as cordis-rs; the library crate is still named cordis, so imports remain use cordis::....
The minimum supported Rust version (MSRV) is Rust 1.85, and the crate uses Rust 2024 Edition. The crate has no external dependencies.
Rust version policy
- MSRV: Rust 1.85. CI and releases must continue to compile and test on this exact version.
- Development toolchain: the latest stable Rust release is used for formatting, Clippy, documentation, and forward-compatibility testing.
- Review cadence: the MSRV is reviewed every six months, around February and August. A review does not imply an automatic version increase.
- Review factors: maintainers consider the compiler shipped by stable Linux distributions, requirements of official plugins and downstream projects, useful language or standard-library improvements, dependency/security constraints, and toolchain versions actually used by downstream users.
- Version changes: the MSRV is raised only when there is a concrete maintenance or ecosystem benefit. An increase is documented in the changelog and release notes and is made in a minor release, never silently in a patch release.
- Workspace consistency: official Cordis crates and plugins should use one shared MSRV unless a documented platform constraint requires an exception.
Quick start
use ;
use ;
use Arc;
;
Dependency injection and reload
Inject controls whether a plugin may be active. Service changes reconcile consumers immediately and deterministically.
use ;
A plugin can attach per-service intercept config as part of its inject declaration:
use ;
let inject = new.require_with;
Scoped services
use ;
Effects
Every effect is single-shot and fiber-owned. Fiber unloading runs effects in reverse registration order. Cleanup errors are logged and do not prevent the remaining effects from running.
use ;
let root = new;
let handle = root.effect_infallible?;
assert_eq!;
handle.dispose?; // early cleanup
handle.dispose?; // no-op
# Ok::
Use effect_async() or AsyncDisposer::from_async() for asynchronous cleanup. A child plugin, listener, provided service, logger exporter, or accessor is internally registered as the same kind of effect.
Events
Arguments and bail values are Values. None means “continue”; Some(value) means “bail”.
use block_on;
use ;
let root = new;
let _listener = root.on?;
let answer = root.events
.bail?
.unwrap
.?;
assert_eq!;
block_on?;
# Ok::
Dispatch modes:
emit: invoke in order and synchronously propagate the first error.parallel: poll every listener concurrently and aggregate errors.serial: await in order and stop on the first bail value.bail: synchronous ordered bail.waterfall/waterfall_async: each listener receivesevent.next()and may wrap or veto the rest of the chain.
Reflection
Normal Rust code should prefer typed services. Value, Accessor, and alias() support dynamic framework/loader use cases:
use ;
use ;
let root = new;
let state = new;
let read = state.clone;
let write = state.clone;
let _property = root.accessor?;
root.set?;
assert_eq!;
# Ok::
Logger
The logger keeps a bounded chronological buffer and sends structured Messages to effect-owned exporters. It supports Cordis placeholders (%s, %d, %i, %f, %o, %O, %c, %C, and %%), per-name levels, custom formatters, ANSI name colors, and logger intercepts.
use ;
let root = new;
let mut config = default;
config.levels.insert;
let render = config.clone;
let _exporter = root.logger_service.exporter_fn?;
root.named_logger.info;
# Ok::
Writing a custom plugin
Closure adapters cover most plugins. Dynamic loaders can implement the object-safe trait directly:
use BoxFuture;
use ;
Override validate_config() to normalize config or return CordisError::validation(...). service_sync() and service_async() adapt constructors returning a type that implements Service.
Runtime notes
The original TypeScript implementation schedules lifecycle work through promises. This crate deliberately reconciles lifecycle transitions eagerly: provide, effect disposal, restart, and update return after affected fibers settle. This makes behavior deterministic without requiring Tokio or another executor. Fiber::await_ready, async event modes, async plugins, and async disposers remain available.
Executor-independent futures work everywhere. If a future creates runtime-specific resources (for example tokio::time::sleep), call Cordis while that runtime is entered.
Two consequences of the eager model: Fiber::wait() reports the settled state instead of suspending until dependencies arrive — it returns an error for Pending or disposed fibers. And futures driven by Cordis run on a small blocking executor while a lifecycle transition lock is held, so plugin apply callbacks and disposers must only await work that completes on other threads (never same-thread channels or spawn_blocking joins).
Fiber::update() mirrors upstream on inactive fibers: on an Active fiber it validates the new config, restarts, and reports the startup outcome; on a Pending or Failed fiber it stores the config and reconciles without waiting, so Ok(()) only means the config was accepted — inspect state()/error() for the outcome of the activation it schedules.
A panic in a plugin apply, disposer, or event listener propagates to the caller of the lifecycle operation that triggered it. Internal mutexes recover from poisoning, and a fiber interrupted mid-transition stays in Loading/Unloading — with already-registered effects still owned — until the next lifecycle event or dispose settles it. Context and Fiber do not implement UnwindSafe because their trait objects cannot prove it; when a plugin must not take down its caller, isolate it with std::panic::catch_unwind(std::panic::AssertUnwindSafe(...)).
Project layout
The source mirrors the upstream package:
src/
├── context.rs # root/child context and scope overlays
├── events.rs # event bus and five dispatch modes
├── fiber.rs # plugin lifecycle and effect ownership
├── logger.rs # messages, formatters, buffer, exporters
├── reflect.rs # scoped service store and computed properties
├── registry.rs # Plugin, Inject, runtime records
├── service.rs # typed service and constructor adapters
├── effect.rs # disposers, handles, diagnostic trees
├── value.rs # Arc<dyn Any> values
└── utils.rs # boxed futures, small executor
Development
# MSRV compatibility
# Latest stable quality and forward-compatibility checks
RUSTDOCFLAGS="-D warnings"
License
MIT. The architecture and behavior are based on Cordis by Shigma and the DeepSeek Harness vendored implementation.