Skip to content

Kernel reference ​

Package: @kestrel/kernel · no runtime dependencies · Node 22+.

The kernel provides contexts, services, events, scopes and plugin handles. Everything else in Kestrel is built on these five concepts.

createContext ​

ts
function createContext(options?: CreateContextOptions): Context;
ts
interface CreateContextOptions {
  name?: string;             // default "root"
  logger?: Logger;           // default createLogger()
  services?: Record<string, unknown>; // installed before any plugin runs
}

The root context owns the service registry. createContext installs the logger as log and then every entry in services.

Context ​

ts
class Context implements ContextLike {
  readonly name: string;
  readonly root: Context;
  readonly parent?: Context;
  readonly logger: Logger;
  readonly scope: Scope;

  get<T>(key: string): T;
  has(key: string): boolean;
  provide<T>(key: string, value: T, options?: { replace?: boolean }): Disposer;
  effect(setup: () => Disposer | void): Disposer;
  inject(keys: readonly string[], callback: (context: Context) => MaybePromise<Disposer | void>): Disposer;
  on(event, listener, options?: { prepend?: boolean; once?: boolean }): Disposer;
  emit(event, ...args): void;
  plugin<T>(definition: PluginDefinition<T>, config?: T): PluginHandle;
  dispose(): Promise<void>;
}

get / has ​

get searches the current context, then its ancestors. A missing key throws ServiceError with the key and the context name. has performs the same walk without throwing.

ts
const tools = context.get<ToolRegistry>("tools");
if (context.has("memory")) { /* optional capability */ }

provide ​

Installs a value in the root registry and returns a disposer that removes it when this context still owns it.

  • Throws ServiceError if the key exists and was provided by a different context, unless { replace: true }.
  • Re-providing from the same context replaces the value and returns a disposer for the new value.
  • Removing a service emits service/removed, which deactivates injectors that depend on it.

effect ​

Runs setup immediately and registers the returned disposer on this context's scope.

ts
context.effect(() => {
  const timer = setInterval(poll, 5_000);
  return () => clearInterval(timer);
});

If setup throws, the error propagates to the caller. Effects run in registration order and unwind in reverse order.

inject ​

Registers a callback that runs whenever all listed service keys exist. The callback may return a disposer, which runs if any dependency disappears or the context is disposed.

ts
context.inject(["models", "tools"], (ctx) => {
  ctx.get<ToolRegistry>("tools").register(tool);
});
  • The callback is not run synchronously if dependencies are missing.
  • It is re-run if dependencies are removed and later provided again.
  • Concurrent scheduling is guarded: a callback is not entered twice.
  • Errors in the callback are logged and do not crash the process.

on / emit ​

Events bubble from the context up through its ancestors. Listeners on a parent observe events emitted by children. once removes the listener after the first call; prepend puts it at the front of the listener list. A throwing listener is logged and skipped.

plugin ​

ts
const handle = context.plugin(definition, config);
await handle.ready();      // resolves after apply
await handle.dispose();    // unwinds the plugin scope
handle.snapshot();         // { id, name, version, state, dependencies, error? }

The child context is named after the definition. The handle is registered with the parent, so disposing the parent disposes the child. Calling plugin with a name that already exists under the same parent returns the existing handle.

dispose ​

Disposes child plugins (in reverse creation order), injectors, listeners, effects and services. The returned promise resolves after all disposers have run. Errors are logged, not thrown. Disposing twice is a no-op.

Scope ​

ts
class Scope {
  constructor(name: string, parent?: Scope);
  add(disposer: Disposer): void;
  dispose(): Promise<void>;
  disposed: boolean;
}

A scope is an ordered disposer bag. Child scopes must be disposed before the parent; the kernel handles this ordering internally. Disposers are invoked sequentially in reverse registration order, and a failing disposer does not prevent the rest from running.

Events ​

EventArgumentsWhen
plugin/loadingsnapshotapply is about to run.
plugin/activesnapshotapply resolved.
plugin/errorsnapshot, errorapply threw or timed out.
plugin/disposedsnapshotAll plugin disposers finished.
service/providedkey, valueA service became available.
service/removedkeyA service was removed.
context/disposed—The context finished disposing.

Feature packages augment EventMap and ServiceMap through declaration merging.

Logger ​

ts
interface LoggerOptions {
  level?: LogLevel;                 // trace | debug | info | warn | error | silent
  format?: "pretty" | "json";
  bindings?: Record<string, unknown>;
  sink?: (line: string, level: LogLevel) => void;
}

function createLogger(options?: LoggerOptions): Logger;
function silencedLogger(): Logger;

createLogger reads KESTREL_LOG_LEVEL and KESTREL_LOG_FORMAT when options are omitted. child() merges bindings; the console uses logger.child({ component: "sessions" }) to tag records.

Errors ​

ClassFieldsRaised when
KestrelErrorcodeBase class for platform errors.
ServiceErrorservice, contextA service is missing or already owned.
PluginErrorpluginA plugin cannot be loaded or applied.
ContextDisposedError—An operation touches a disposed context.

Purity ​

The kernel imports only node:util (for logging) and no network or filesystem APIs. It can run in a worker, a test, or an embedded runtime without side effects.

Released under the MIT License.