@wooksjs/event-core
Version:
@wooksjs/event-core
467 lines (456 loc) • 17.9 kB
TypeScript
/**
* Logger interface used throughout Wooks. Passed via `EventContextOptions`
* and accessed with `useLogger()`. Compatible with `@prostojs/logger` and
* most standard loggers.
*/
interface Logger {
info(msg: string, ...args: unknown[]): void;
warn(msg: string, ...args: unknown[]): void;
error(msg: string, ...args: unknown[]): void;
debug(msg: string, ...args: unknown[]): void;
/** Creates a child logger scoped to a topic. Matches `ProstoLogger.createTopic()`. */
createTopic?: (name: string) => Logger;
}
/**
* A typed, writable context slot. Created with `key<T>(name)`.
* Use `ctx.set(k, value)` to store and `ctx.get(k)` to retrieve.
*/
interface Key<T> {
readonly _id: number;
readonly _name: string;
/** @internal Type brand — not used at runtime. */
readonly _T?: T;
}
/**
* A lazily-computed, read-only context slot. Created with `cached<T>(fn)`.
* The factory runs once per context on first `ctx.get()` call; the result is cached.
*/
interface Cached<T> {
readonly _id: number;
readonly _name: string;
readonly _fn: (ctx: EventContext) => T;
/** @internal Type brand — not used at runtime. */
readonly _T?: T;
}
/** Union type for any context slot — either a writable `Key` or a computed `Cached`. */
type Accessor<T> = Key<T> | Cached<T>;
/**
* Type-level marker used in `defineEventKind` schemas. Each `slot<T>()`
* becomes a typed `Key<T>` on the resulting `EventKind`. No runtime behavior.
*/
interface SlotMarker<T> {
/** @internal Type brand — not used at runtime. */
readonly _T?: T;
}
/**
* Declares the shape of an event type — its name and typed seed slots.
* Created with `defineEventKind(name, schema)`.
*
* Use `kind.keys.<prop>` to access typed context keys for each slot.
*/
interface EventKind<S extends Record<string, SlotMarker<any>>> {
/** Unique event kind name (e.g. `'http'`, `'cli'`). */
readonly name: string;
/** Typed context keys for each slot in the schema. */
readonly keys: {
[K in keyof S]: S[K] extends SlotMarker<infer V> ? Key<V> : never;
};
/** @internal Pre-computed entries for fast `seed()`. */
readonly _entries: Array<[string, Key<unknown>]>;
}
/**
* Extracts the seed values type for an `EventKind`. This is the object
* shape passed to `ctx.seed(kind, seeds)` or `createEventContext(opts, kind, seeds, fn)`.
*/
type EventKindSeeds<K> = K extends EventKind<infer S> ? {
[P in keyof S]: S[P] extends SlotMarker<infer V> ? V : never;
} : never;
/** Options for creating an {@link EventContext}. */
interface EventContextOptions {
/** Logger instance available to all composables via `useLogger()`. */
logger: Logger;
/** Optional parent context. Enables transparent read-through and scoped writes. */
parent?: EventContext;
}
/**
* Per-event container for typed slots, propagated via `AsyncLocalStorage`.
* Composables read and write data through `get`/`set` using typed `Key` or `Cached` accessors.
*
* Supports a parent chain: when a slot is not found locally, `get()` traverses
* parent contexts. `set()` writes to the nearest context that already holds the slot,
* or locally if the slot is new.
*
* Typically created by adapters (HTTP, CLI, etc.) — application code
* interacts with it indirectly through composables.
*
* @example
* ```ts
* const ctx = new EventContext({ logger })
* ctx.set(userIdKey, '123')
* ctx.get(userIdKey) // '123'
* ```
*/
declare class EventContext {
/** Logger instance for this event. */
readonly logger: Logger;
/** Parent context for read-through and scoped writes. */
readonly parent?: EventContext;
private slots;
constructor(options: EventContextOptions);
/**
* Controls whether `get()`, `has()`, and `set()` traverse the parent chain
* for a given slot. Override in subclasses to isolate specific slots —
* returning `false` forces local computation/storage, preventing inheritance.
*
* @param _id - The numeric slot identifier (`accessor._id`)
* @returns `true` to allow parent traversal (default), `false` to block it
*/
protected _shouldTraverseParent(_id: number): boolean;
/**
* Reads a value from a typed slot.
* - For `Key<T>`: returns the previously `set` value, checking parent chain if not found locally.
* - For `Cached<T>`: returns a cached result from this context or any parent. If not found
* anywhere, runs the factory on first access, caches locally, and returns the result.
* Throws on circular dependencies. Errors are cached and re-thrown on subsequent access.
*/
get<T>(accessor: Key<T> | Cached<T>): T;
/**
* Writes a value to a typed slot. If the slot already exists somewhere in the
* parent chain, the value is written there. Otherwise, it is written locally.
*/
set<T>(key: Key<T> | Cached<T>, value: T): void;
/**
* Returns `true` if the slot has been set or computed in this context or any parent.
*/
has(accessor: Accessor<any>): boolean;
/**
* Reads a value from a typed slot in this context only, ignoring parents.
* Same semantics as `get()` but without parent chain traversal.
*/
getOwn<T>(accessor: Key<T> | Cached<T>): T;
/**
* Writes a value to a typed slot in this context only, ignoring parents.
*/
setOwn<T>(key: Key<T> | Cached<T>, value: T): void;
/**
* Returns `true` if the slot has been set or computed in this context only.
*/
hasOwn(accessor: Accessor<any>): boolean;
/**
* Seeds an event kind's slots into this context. Sets the event type key
* and populates all slots declared in the kind's schema.
*
* @param kind - The event kind (from `defineEventKind`)
* @param seeds - Values for each slot in the kind's schema
* @param fn - Optional callback to run after seeding (returned value is forwarded)
*/
seed<S extends Record<string, any>>(kind: EventKind<S>, seeds: EventKindSeeds<EventKind<S>>): void;
seed<S extends Record<string, any>, R>(kind: EventKind<S>, seeds: EventKindSeeds<EventKind<S>>, fn: () => R): R;
/** Walk the parent chain looking for a set slot. Returns `undefined` if not found. */
private _findSlot;
/** Set value in the first context in the chain that has this slot. Returns true if found. */
private _setIfExists;
}
/**
* Creates a typed, writable context slot. Use `ctx.set(k, value)` to store
* and `ctx.get(k)` to retrieve. Throws if read before being set.
*
* @param name - Debug label (shown in error messages, not used for lookup)
*
* @example
* ```ts
* const userIdKey = key<string>('userId')
* ctx.set(userIdKey, '123')
* ctx.get(userIdKey) // '123'
* ```
*/
declare function key<T>(name: string): Key<T>;
/**
* Creates a lazily-computed, read-only context slot. The factory runs once
* per `EventContext` on first `ctx.get(slot)` call; the result is cached
* for the context lifetime. Errors are also cached and re-thrown.
*
* @param fn - Factory receiving the current `EventContext`, returning the value to cache
*
* @example
* ```ts
* const parsedUrl = cached((ctx) => new URL(ctx.get(rawUrlKey)))
* // first call computes, subsequent calls return cached result
* ctx.get(parsedUrl)
* ```
*/
declare function cached<T>(fn: (ctx: EventContext) => T): Cached<T>;
/**
* Creates a parameterized cached computation. Maintains a `Map<K, V>` per
* event context — one cached result per unique key argument.
*
* @param fn - Factory receiving the lookup key and `EventContext`, returning the value to cache
* @returns A function `(key: K, ctx?: EventContext) => V` that computes on first call per key
*
* @example
* ```ts
* const parseCookie = cachedBy((name: string, ctx) => {
* const raw = ctx.get(cookieHeaderKey)
* return parseSingleCookie(raw, name)
* })
*
* parseCookie('session') // computed and cached for 'session'
* parseCookie('theme') // computed and cached for 'theme'
* parseCookie('session') // returns cached result
* ```
*/
declare function cachedBy<K, V>(fn: (key: K, ctx: EventContext) => V): (key: K, ctx?: EventContext) => V;
/**
* Type-level marker used inside `defineEventKind` schemas. Each `slot<T>()`
* becomes a typed `Key<T>` on the resulting `EventKind`. Has no runtime behavior.
*
* @example
* ```ts
* const httpKind = defineEventKind('http', {
* req: slot<IncomingMessage>(),
* response: slot<HttpResponse>(),
* })
* ```
*/
declare function slot<T>(): SlotMarker<T>;
/**
* Declares a named event kind with typed seed slots. The returned object
* contains `keys` — typed accessors for reading seed values from context —
* and is passed to `ctx.seed(kind, seeds)` or `createEventContext()`.
*
* @param name - Unique event kind name (e.g. `'http'`, `'cli'`, `'workflow'`)
* @param schema - Object mapping slot names to `slot<T>()` markers
* @returns An `EventKind` with typed `keys` for context access
*
* @example
* ```ts
* const httpKind = defineEventKind('http', {
* req: slot<IncomingMessage>(),
* response: slot<HttpResponse>(),
* })
*
* // Access typed seed values:
* const req = ctx.get(httpKind.keys.req) // IncomingMessage
* ```
*/
declare function defineEventKind<S extends Record<string, SlotMarker<any>>>(name: string, schema: S): EventKind<S>;
/**
* A composable function created by {@link defineWook}. Callable as `(ctx?) => T`,
* with an exposed `_slot` for advanced use cases such as slot isolation in child contexts.
*/
interface WookComposable<T> {
(ctx?: EventContext): T;
/** The underlying `Cached` slot. Useful for building isolation lists in child contexts. */
readonly _slot: Cached<T>;
}
/**
* Creates a composable with per-event caching. The factory runs once per
* `EventContext`; subsequent calls within the same event return the cached result.
*
* This is the recommended way to build composables in Wooks. All built-in
* composables (`useRequest`, `useResponse`, `useCookies`, etc.) are created with `defineWook`.
*
* @param factory - Receives the `EventContext` and returns the composable's public API
* @returns A composable function with an exposed `_slot` for isolation
*
* @example
* ```ts
* export const useCurrentUser = defineWook((ctx) => {
* const { basicCredentials } = useAuthorization(ctx)
* const username = basicCredentials()?.username
* return {
* username,
* profile: async () => username ? await db.findUser(username) : null,
* }
* })
*
* // In a handler — factory runs once, cached for the request:
* const { username, profile } = useCurrentUser()
* ```
*/
declare function defineWook<T>(factory: (ctx: EventContext) => T): WookComposable<T>;
/**
* No-op base class for observability integration. Subclass and override
* `with()` / `hook()` to add tracing, metrics, or logging around event
* lifecycle points.
*
* The default implementation simply calls the callback with no overhead.
* Replace via `replaceContextInjector()` to enable instrumentation.
*/
declare class ContextInjector<N> {
/**
* Wraps a callback with optional named attributes for observability.
* Default implementation just calls `cb()` — override for tracing.
*/
with<T>(name: N, attributes: Record<string, string | number | boolean>, cb: () => T): T;
with<T>(name: N, cb: () => T): T;
/**
* Hook called by adapters at specific lifecycle points (e.g., after route lookup).
* Default implementation is a no-op — override for observability.
*/
hook(_method: string, _name: 'Handler:not_found' | 'Handler:routed', _route?: string): void;
}
/**
* Returns the current `ContextInjector` instance, or `null` if none has been installed.
* Used internally by adapters to wrap lifecycle events.
*/
declare function getContextInjector<N = TContextInjectorHooks>(): ContextInjector<N> | null;
/**
* Replaces the global `ContextInjector` with a custom implementation.
* Use this to integrate OpenTelemetry or other observability tools.
*
* @param newCi - Custom `ContextInjector` subclass instance
*
* @example
* ```ts
* class OtelInjector extends ContextInjector<string> {
* with<T>(name: string, attrs: Record<string, any>, cb: () => T): T {
* return tracer.startActiveSpan(name, (span) => {
* span.setAttributes(attrs)
* try { return cb() } finally { span.end() }
* })
* }
* }
* replaceContextInjector(new OtelInjector())
* ```
*/
declare function replaceContextInjector(newCi: ContextInjector<string>): void;
/**
* Resets the global `ContextInjector` back to `null` (no-op default).
* Useful for tests or when disabling instrumentation.
*/
declare function resetContextInjector(): void;
/** Built-in hook names used by the framework. */
type TContextInjectorHooks = 'Event:start';
/** Context key for route parameters. Set by adapters after route matching. */
declare const routeParamsKey: Key<Record<string, string | string[]>>;
/** Context key for the event type name (e.g. `'http'`, `'cli'`). Set by `ctx.seed()`. */
declare const eventTypeKey: Key<string>;
/**
* Returns the route parameters for the current event. Works with HTTP
* routes, CLI commands, workflow steps — any adapter that sets `routeParamsKey`.
*
* @param ctx - Optional explicit context (defaults to `current()`)
* @returns Object with `params` (the full params record) and `get(name)` for typed access
*
* @example
* ```ts
* app.get('/users/:id', () => {
* const { params, get } = useRouteParams<{ id: string }>()
* console.log(get('id')) // typed as string
* })
* ```
*/
declare function useRouteParams<T extends Record<string, string | string[]> = Record<string, string | string[]>>(ctx?: EventContext): {
params: T;
get: <K extends keyof T>(name: K) => T[K];
};
/**
* Provides a unique, per-event identifier. The ID is a random UUID, generated
* lazily on first `getId()` call and cached for the event lifetime.
*
* @param ctx - Optional explicit context (defaults to `current()`)
*
* @example
* ```ts
* const { getId } = useEventId()
* logger.info(`Request ${getId()}`)
* ```
*/
declare function useEventId(ctx?: EventContext): {
getId: () => string;
};
/**
* Runs a callback with the given `EventContext` as the active context.
* All composables and `current()` calls inside `fn` will resolve to `ctx`.
*
* @param ctx - The event context to make active
* @param fn - Callback to execute within the context scope
* @returns The return value of `fn`
*
* @example
* ```ts
* const ctx = new EventContext({ logger })
* run(ctx, () => {
* // current() returns ctx here
* const logger = useLogger()
* })
* ```
*/
declare function run<R>(ctx: EventContext, fn: () => R): R;
/**
* Returns the active `EventContext` for the current async scope.
* Throws if called outside an event context (e.g., at module level).
*
* All composables use this internally. Prefer composables over direct `current()` access.
*
* @throws Error if no active event context exists
*/
declare function current(): EventContext;
/**
* Returns the active `EventContext`, or `undefined` if none is active.
* Use this when context availability is uncertain (e.g., in code that may
* run both inside and outside an event handler).
*/
declare function tryGetCurrent(): EventContext | undefined;
/**
* Returns the logger for the current event context.
*
* When called with a `topic` string, creates a child logger via
* `logger.createTopic()` (if supported). Falls back to the base
* logger when `createTopic` is not available.
*
* @example
* ```ts
* const logger = useLogger()
* logger.info('Processing request')
*
* const scoped = useLogger('auth')
* scoped.warn('Token expired')
* ```
*/
declare function useLogger(): Logger;
declare function useLogger(topic: string): Logger;
declare function useLogger(ctx: EventContext): Logger;
declare function useLogger(topic: string, ctx: EventContext): Logger;
/**
* Creates a new `EventContext`, makes it the active context via
* `AsyncLocalStorage`, and runs `fn` inside it.
*
* @param options - Context options (must include `logger`)
* @param fn - Callback to execute within the new context
* @returns The return value of `fn`
*
* The kindless overload is a convenience for tests that need a context scope
* without declaring an event kind. Production code should always provide a kind.
*
* @example
* ```ts
* createEventContext({ logger }, () => {
* // composables work here
* })
* ```
*/
declare function createEventContext<R>(options: EventContextOptions, fn: () => R): R;
/**
* Creates a new `EventContext` with an event kind, seeds the kind's slots,
* and runs `fn` inside the context.
*
* @param options - Context options (must include `logger`)
* @param kind - Event kind (from `defineEventKind`)
* @param seeds - Seed values for the event kind's slots
* @param fn - Callback to execute within the new context
* @returns The return value of `fn`
*
* @example
* ```ts
* const httpKind = defineEventKind('http', { req: slot<IncomingMessage>() })
*
* createEventContext({ logger }, httpKind, { req: incomingMessage }, () => {
* const req = current().get(httpKind.keys.req)
* })
* ```
*/
declare function createEventContext<S extends Record<string, any>, R>(options: EventContextOptions, kind: EventKind<S>, seeds: EventKindSeeds<EventKind<S>>, fn: () => R): R;
export { ContextInjector, EventContext, cached, cachedBy, createEventContext, current, defineEventKind, defineWook, eventTypeKey, getContextInjector, key, replaceContextInjector, resetContextInjector, routeParamsKey, run, slot, tryGetCurrent, useEventId, useLogger, useRouteParams };
export type { Accessor, Cached, EventContextOptions, EventKind, EventKindSeeds, Key, Logger, SlotMarker, TContextInjectorHooks, WookComposable };