1
import type { Dict } from '@deepseek-ai/cosmokit'2
import { EventsService } from './events.ts'3
import { LoggerService } from './logger.ts'4
import { ReflectService } from './reflect.ts'5
import { RegistryService, type InjectKey } from './registry.ts'6
import { getTraceable, symbols } from './utils.ts'7
import { Fiber } from './fiber.ts'9
/**10
* Public shape of a Cordis context.11
*12
* The concrete `Context` class is proxied at runtime, so this interface is13
* augmented by core services and plugins to describe the properties that may14
* be read from `ctx`.15
*/16
export interface Context {17
/** Isolation map: service name → scope label. Lookups for a name resolve within its label. */18
[symbols.isolate]: Dict<symbol>19
/** Intercept map: service name → config merged into that service's per-plugin config. */20
[symbols.intercept]: Dict21
/** The root context of the application (every child context shares it). @experimental */22
root: this23
/** Base URL used to resolve relative plugin/module specifiers, if the runtime sets one. */24
baseUrl?: string25
/** The event bus. Its methods are also mixed onto `ctx` (`ctx.on`, `ctx.emit`, ...). */26
events: EventsService27
/** The logging service. Call `ctx.logger(name)` for a named logger. */28
logger: LoggerService29
/** The reflection layer backing the context proxy (`ctx.get`, `ctx.provide`, ...). */30
reflect: ReflectService31
/** The plugin registry. Its methods are mixed onto `ctx` (`ctx.plugin`, `ctx.inject`). */32
registry: RegistryService33
}35
/**36
* Root and child dependency containers for Cordis plugins.37
*38
* A context is a proxy: normal property reads go through the service resolver,39
* while `extend()`, `isolate()`, and `intercept()` create scoped child40
* contexts without mutating their parent.41
*/42
export class Context {43
/** Symbol key under which a disposer exposes its {@link EffectMeta} diagnostics tree. */44
static readonly effect: unique symbol = symbols.effect45
/** Symbol key for a context's listener filter, consulted on every event dispatch. */46
static readonly filter: unique symbol = symbols.filter47
/** Symbol key of the isolation map (see the `Context[symbols.isolate]` property). */48
static readonly isolate: unique symbol = symbols.isolate49
/** Symbol key of the intercept map (see the `Context[symbols.intercept]` property). */50
static readonly intercept: unique symbol = symbols.intercept52
/**53
* Returns true for Cordis context proxies and context prototypes.54
*55
* Works across realms and across multiple copies of cordis, because the56
* brand is keyed by a global symbol rather than by `instanceof`.57
*58
* @param value — the value to test.59
* @returns `true` if `value` is a Cordis context, narrowing its type.60
*/61
static is(value: any): value is Context {62
return !!value?.[Context.is as any]63
}65
static {66
Context.is[Symbol.toPrimitive] = () => Symbol.for('cordis.is')67
Context.prototype[Context.is as any] = true68
}70
/** Create the root context and install the built-in services. */71
constructor() {72
this[symbols.isolate] = Object.create(null)73
this[symbols.intercept] = Object.create(null)74
const self = new Proxy<this>(this, ReflectService.handler)75
this.root = self76
this.baseUrl = undefined77
this.fiber = new Fiber(self, {}, Object.create(null), null, () => [])78
this.reflect = new ReflectService(self)79
this.registry = new RegistryService(self)80
this.events = new EventsService(self)81
this.logger = new LoggerService(self)82
this.fiber._disposables.clear()83
return self84
}86
[Symbol.for('nodejs.util.inspect.custom')]() {87
return `Context <${this.fiber.name}>`88
}90
/**91
* Create a child context with extra metadata on top of the current scope.92
*93
* The child prototypally inherits every property of this context; own94
* properties of `meta` shadow the inherited ones. The parent is not mutated.95
*96
* @param meta — own properties (including symbol keys) to define on the child.97
* @returns a child context inheriting from this one.98
*/99
extend(meta = {}): this {100
const shadow = Reflect.getOwnPropertyDescriptor(this, symbols.shadow)?.value101
const self = Object.create(getTraceable(this, this))102
for (const prop of Reflect.ownKeys(meta)) {103
Object.defineProperty(self, prop, Reflect.getOwnPropertyDescriptor(meta, prop)!)104
}105
if (!shadow) return self106
return Object.assign(Object.create(self), { [symbols.shadow]: shadow })107
}109
/**110
* Create a child context with an independent service scope for `name`.111
*112
* Below the returned context, reads and writes of the service `name`113
* resolve against the new label instead of the parent's, so a different114
* implementation can be provided without affecting the parent scope.115
* Passing the same `label` to two `isolate()` calls joins their scopes.116
*117
* @param name — the service name to isolate.118
* @param label — scope label to join; defaults to a fresh unique symbol.119
* @returns a child context whose `name` service resolves in the new scope.120
*/121
isolate(name: string, label?: symbol) {122
const shadow = Object.create(this[symbols.isolate])123
shadow[name] = label ?? Symbol(name)124
return this.extend({ [symbols.isolate]: shadow })125
}127
/**128
* Add service-specific intercept config for plugins started below this129
* context.130
*131
* Plugins loaded under the returned context see `config` merged into the132
* service's resolved config (ancestor entries first; see133
* `Service[symbols.resolveConfig]`). The parent context is not affected.134
*135
* @param name — the service name whose config to intercept.136
* @param config — the intercept config to merge for that service.137
* @returns a child context carrying the additional intercept entry.138
*/139
intercept<K extends InjectKey>(name: K, config: Context[K] extends { [symbols.config]: infer T } ? T : never): this140
intercept(name: string, config: any): this141
intercept(name: string, config: any) {142
const intercept = Object.create(this[symbols.intercept])143
intercept[name] = config144
return this.extend({ [symbols.intercept]: intercept })145
}146
}