Chapter 10
Cordis: reading the source
Core is about 1,900 lines in packages/core/src/: context.ts (78), fiber.ts (488), reflect.ts (283), registry.ts (214), service.ts (80), events.ts (188), logger.ts, utils.ts. Line numbers below are from cordiverse/cordis master as of 2026-09-03. The paper's Table 2 names differ a little from the code:
| Theory | Paper (§5) | Code |
|---|---|---|
| effectΓ, ℑΓ | ctx.effect(cb) | Fiber.effect — fiber.ts:275–340 |
| set(k,v) / get(k) | ctx.set / ctx.get | ReflectService.provide / get — reflect.ts:177, 152 |
| isolate, intercept | ctx.isolate, ctx.intercept | context.ts:65–77, both via extend |
| O-Insert + O-Retire | ctx.use | ctx.plugin — registry.ts:193, fiber.ts:170–199 |
| d / p | fiber.inject / provide | same |
| ω committed view | fiber.committed | fiber.store |
| target_n | fiber.target | runner.epoch — a string ':uid:uid…', '__INACTIVE__' for ⊥ |
| θ | fiber.state | FiberState: PENDING (Inactive), LOADING (Reloading), ACTIVE, FAILED, DISPOSED, UNLOADING |
| transition in flight | fiber.inertia | same (a Promise) |
10.1 Effect tracking — Fiber.effect
// fiber.ts (abridged)
effect(execute: () => Effect, label = 'anonymous'): any {
this.assertActive()
const disposables: Disposable[] = []
const dispose = () => {
let task
for (const dispose of disposables.splice(0).reverse()) { // LIFO
task = task ? task.then(dispose) : dispose()
}
return task
}
const runner = { execute, epoch: true, collect: d => disposables.push(d), ... }
let task = this._execute(runner) // drives sync/async iterators, collecting yields
const wrapper = () => { if (!runner.epoch) return; runner.epoch = false; return task ? task.then(dispose) : dispose() }
disposables.push(this._disposables.push(wrapper)) // child's inverse is an effect on the parent (∂²Γ)
return wrapper
}
_execute (:229–273) accepts a disposer, an iterable of disposers, a promise of one, or an async iterable — the 𝔈Γ/ℑΓ ad-hoc polymorphism of §5.1.1. In the async-iterable branch it checks runner.epoch !== oldEpoch before each iter.next() — the step-boundary interruption. Flipping epoch makes dispose fire at most once ("firing twice would apply an inverse at a state no application of the effect produced").
That the disposer actually reverts is the author's obligation (the 𝔈Γ* witness). §5.1.1 says so in as many words; the runtime verifies nothing. Likewise the commutativity witness of a key.
10.2 Coeffects — ReflectService
// reflect.ts (abridged)
provide(name, value?, check?) {
return this.ctx.fiber.effect(() => { // set(k,v) IS an effect
this.ctx.root[symbols.isolate][name] ??= Symbol(name) // realm symbol ρ(k)
const key = this.ctx[symbols.isolate][name]
const impl = { name, value, fiber: this.ctx.fiber, check }
if (this.store[key]) throw new Error(`service "${name}" has been registered at <…>`) // k ∉ dom σ
this.store[key] = impl; this.ctx.fiber.store![name] = impl
if (this.ctx.fiber.state === FiberState.ACTIVE) this.notify([name])
return async () => {
delete this.store[key]
const fibers = this.notify([name])
await Promise.allSettled(fibers.map(f => f.await())) // the L-Unload guard: wait for dependents
delete this.ctx.fiber.store![name]
}
}, `ctx.provide(${JSON.stringify(name)})`)
}
notify(names, filter = sameRealm) { // Algorithm 3
for (const runtime of this.ctx.registry.values())
for (const fiber of runtime.fibers) {
let hasUpdate = false
for (const name of names)
if (name in fiber.inject && filter(fiber.ctx, name)) { hasUpdate = true; fiber._checkImpl(name) }
if (hasUpdate) { fiber._refresh(); fibers.push(fiber) }
}
...
}
_getImpl(name, strict = true) {
const impl = this.store[this.ctx[symbols.isolate][name]]
if (strict && impl.fiber.state !== FiberState.ACTIVE) return // only ACTIVE fibers provide
return impl
}
The Proxy get trap (:63–100) is Algorithm 6: walk up the fiber chain; return from the first fiber.store that binds the key; a fiber that declares the key but hasn't committed it throws cannot get required service "x" in inactive context; reaching root without a declaration throws cannot get property "x" without inject. The set trap refuses writes without provide. mixin (:241–267) is how ctx.on/plugin/effect/get/provide… appear on the context — accessors delegating to events, registry, fiber, reflect.
10.3 Isolation and interception — derived, literally
// context.ts
isolate(name: string, label?: symbol) {
const shadow = Object.create(this[symbols.isolate]); shadow[name] = label ?? Symbol(name)
return this.extend({ [symbols.isolate]: shadow })
}
intercept(name: string, config: any) {
const intercept = Object.create(this[symbols.intercept]); intercept[name] = config
return this.extend({ [symbols.intercept]: intercept })
}
// service.ts — right-biased merge up the prototype chain
[symbols.resolveConfig](base?, head?) {
let intercept = this.ctx[Context.intercept]; const configs = []
while (this.name in intercept) {
if (Object.hasOwn(intercept, this.name)) configs.unshift(intercept[this.name])
intercept = Object.getPrototypeOf(intercept)
}
return this['Config']?.merge ? this['Config'].merge(...configs) : Object.assign({}, ...configs)
}
10.4 Lifecycle — Fiber
// fiber.ts (abridged)
_refresh() { // recompute the target-view digest
let epoch = ''
for (const name of Object.keys(this.inject)) {
const impl = this._store[name]
if (!impl) { epoch = INACTIVE; break }
epoch += ':' + impl.fiber.uid // provider identity, not value
}
this._setEpoch(epoch)
}
private _setEpoch(epoch) {
if (epoch === this._runner.epoch || this._error) return // FAILED only recovers through update()
this._runner.epoch = epoch
if (this.inertia) return // inertial: finish the transition first
this._updateState(() => epoch !== INACTIVE && oldEpoch === INACTIVE
? (this.inertia = this._reload(), FiberState.LOADING)
: (this.inertia = this._unload(), FiberState.UNLOADING))
}
private async _reload() {
this.store = { ...this._store } // commit the view ω
const oldEpoch = this._runner.epoch
try { await this._execute(this._runner) } // guard: epoch stable at each boundary
catch (reason) { this._error = reason; this._runner.epoch = INACTIVE }
this._updateState(() => this._runner.epoch === oldEpoch
? (this.inertia = undefined) // ACTIVE
: (this.inertia = this._unload(), FiberState.UNLOADING)) // target moved: chain into unload
}
private async _unload() {
await Promise.all(this._disposables.clear().map(dispose => ...)) // run inverses
this.store = undefined // discard ω last
this._updateState(() => this._runner.epoch === INACTIVE
? (this.inertia = undefined) // INACTIVE
: (this.inertia = this._reload(), FiberState.LOADING)) // chain back into reload
}
Observe the three lines §5.1.3 says carry Thm 70: _reload commits the view first and _unload discards it last (a fiber reads the same bindings for its whole loaded life, teardown included); _updateState marks UNLOADING before the task is created (L-Leave: stop providing before any inverse is scheduled); and provide's disposer awaits the notified dependents (the guard).
10.5 Instantiation is a tracked effect of the parent
// fiber.ts constructor (abridged)
this.dispose = parent.fiber.effect(() => { // O-Insert as an effect of the parent
const remove = runtime.fibers.push(this)
try { this.config = resolveConfig(runtime, config); this._refresh() }
catch (error) { this._error = error }
return async () => { // its inverse is O-Retire
this.uid = null // DISPOSED; a uid is never reused
...
this._setEpoch(INACTIVE)
while (this.inertia) await this.inertia // wait for the child's in-flight transition
}
}, 'ctx.plugin()')
So unloading a parent cascades to children exactly as Def 52 describes. RegistryService.plugin (registry.ts:193–213) accepts a function, a constructor, or an object with apply, and Inject.resolve normalizes inject: ['a','b'] or {a: cfg} to a dict; per-key config from inject is installed as intercept metadata on the child context (fiber.ts:137–144). update(config) (:478) clears _error — retry is a revision.
10.6 Events
EventsService.on registers through fiber.effect (events.ts:134–141): a listener is a revertible effect, and the hooks table is the canonical commutative key (one entry per registration, removed by identity). Dispatch modes: emit (fire-and-forget), parallel (allSettled, AggregateError), serial (await each, stop at first non-null), bail (sync, stop at first non-null), waterfall (middleware with a once-only next()). The internal/* events (plugin, status, service, update, get, set, listener, dispatch) are what the loader and devtools hang off.