alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,555 lines • 112 kB
JavaScript
import { AsyncLocalStorage } from "node:async_hooks";
import { z as z$1 } from "zod";
import enLocale from "zod/v4/locales/en.js";
import { createMiddleware as createMiddleware$1 } from "alepha";
//#region ../../src/core/constants/KIND.ts
/**
* Used for identifying primitives.
*
* @internal
*/
const KIND = Symbol.for("Alepha.Kind");
//#endregion
//#region ../../src/core/constants/MODULE.ts
/**
* Used for identifying modules.
*
* @internal
*/
const MODULE = Symbol.for("Alepha.Module");
//#endregion
//#region ../../src/core/constants/OPTIONS.ts
/**
* Used for primitives options.
*
* @internal
*/
const OPTIONS = Symbol.for("Alepha.Options");
//#endregion
//#region ../../src/core/errors/AlephaError.ts
/**
* Default error class for Alepha.
*/
var AlephaError = class extends Error {
name = "AlephaError";
};
//#endregion
//#region ../../src/core/errors/CircularDependencyError.ts
var CircularDependencyError = class extends AlephaError {
name = "CircularDependencyError";
constructor(provider, parents) {
super(`Instance not available. Looks like a circular dependency. ? -> ${parents?.map((name) => `${name} -> `).join("")}${provider} -> ?`);
}
};
//#endregion
//#region ../../src/core/errors/ContainerLockedError.ts
var ContainerLockedError = class extends AlephaError {
name = "ContainerLockedError";
constructor(message = "Container is locked. No more providers can be added.") {
super(message);
}
};
//#endregion
//#region ../../src/core/errors/TooLateSubstitutionError.ts
var TooLateSubstitutionError = class extends AlephaError {
name = "TooLateSubstitutionError";
constructor(original, substitution) {
super(`Service already substituted. Please, substitute Service '${original}' with Service '${substitution}' before using it.`);
}
};
//#endregion
//#region ../../src/core/providers/zodAugment.ts
const alias = (ctor, name, target) => {
const proto = ctor?.prototype;
if (!proto || Object.getOwnPropertyDescriptor(proto, name)) return;
Object.defineProperty(proto, name, {
configurable: true,
get() {
return this[target];
}
});
};
alias(z$1.ZodObject, "properties", "shape");
alias(z$1.ZodArray, "items", "element");
alias(z$1.ZodUnion, "anyOf", "options");
{
const proto = z$1.ZodTuple?.prototype;
if (proto && !Object.getOwnPropertyDescriptor(proto, "items")) Object.defineProperty(proto, "items", {
configurable: true,
get() {
return this?._zod?.def?.items ?? [];
}
});
}
//#endregion
//#region ../../src/core/providers/ZodProvider.ts
z$1.config(enLocale());
/** Defaults mirroring TypeProvider's static length caps. */
const Z_LIMITS = {
short: 64,
regular: 255,
long: 1024,
rich: 65535,
arrayMaxItems: 1e3
};
/** Attach metadata + default without changing the schema's inferred type. */
const meta = (schema, options) => {
if (!options) return schema;
const { default: def, ...rest } = options;
let s = schema;
if (def !== void 0) s = s.default(def);
if (Object.keys(rest).length > 0) s = s.meta(rest);
return s;
};
const applyString = (base, o) => {
let s = base;
if (o?.minLength != null) s = s.min(o.minLength);
if (o?.maxLength != null) s = s.max(o.maxLength);
if (o?.pattern) s = s.regex(typeof o.pattern === "string" ? new RegExp(o.pattern) : o.pattern);
return s;
};
/** zod's native `.format` getter (uuid/email/safeint/…), guarded. */
const nativeFmt = (s) => {
try {
return s?.format ?? void 0;
} catch {
return;
}
};
/**
* Read the JSON-Schema-style format tag off a schema (drives ORM columns).
* Prefers our normalized `.meta({ format })` (typebox vocab: `date-time`,
* `int64`, `binary`, …) and falls back to zod's native `.format`.
*/
const fmt = (s) => {
try {
return s?.meta?.()?.format ?? nativeFmt(s);
} catch {
return;
}
};
/** zod's internal type discriminator (`string` | `number` | …), wrapping-safe. */
const defType = (s) => s?._zod?.def?.type;
/** Tag a string-format schema with its JSON-Schema `format` (stays a real `ZodString`). */
const strFmt = (base, format) => meta(base, { format });
const z = {
/**
* Native zod coercion namespace (`z.coerce.number()`, `z.coerce.boolean()`…).
* Used at the HTTP/string boundary (query, headers, env) where everything
* arrives as a string; request bodies and the ORM stay strict (no coercion).
*/
coerce: z$1.coerce,
object: z$1.object,
array: z$1.array,
record: z$1.record,
tuple: z$1.tuple,
union: z$1.union,
string: z$1.string,
number: z$1.number,
boolean: z$1.boolean,
null: z$1.null,
any: z$1.any,
void: z$1.void,
undefined: z$1.undefined,
literal: z$1.literal,
/** Alias for `zod.literal` (legacy `t.const`). */
const: z$1.literal,
enum: z$1.enum,
/** Integer — native `z.int()` (format `safeint`, drives PG INT column). */
integer: z$1.int,
/** Free-form JSON object (`Record<string, any>`). */
json: () => z$1.record(z$1.string(), z$1.any()),
text: (options = {}) => {
const { size = "regular", trim = true, lowercase = false, maxLength, ...rest } = options;
const max = maxLength ?? Z_LIMITS[size] ?? Z_LIMITS.regular;
let s = z$1.string().max(max);
if (trim) s = s.trim();
if (lowercase) s = s.toLowerCase();
return meta(applyString(s, rest), {
...rest,
maxLength: max,
"~options": {
trim,
lowercase
}
});
},
shortText: (options = {}) => z.text({
...options,
size: "short"
}),
longText: (options = {}) => z.text({
...options,
size: "long"
}),
richText: (options = {}) => z.text({
...options,
size: "rich"
}),
email: () => strFmt(z$1.email(), "email"),
uuid: () => strFmt(z$1.guid(), "uuid"),
url: () => strFmt(z$1.url(), "url"),
datetime: () => strFmt(z$1.iso.datetime(), "date-time"),
date: () => strFmt(z$1.iso.date(), "date"),
time: () => strFmt(z$1.iso.time(), "time"),
/** bigint as a validated string (no codec). */
bigint: () => strFmt(z$1.string().regex(/^-?\d+$/), "bigint"),
binary: () => strFmt(z$1.string(), "binary"),
duration: () => strFmt(z$1.string(), "duration"),
e164: () => z.text({ pattern: "^\\+[1-9]\\d{1,14}$" }),
bcp47: () => z.text({ pattern: "^[a-z]{2,3}(?:-[A-Z]{2})?$" }),
constantCase: () => z.text({ pattern: "^[A-Z_-]+$" }),
int32: () => z$1.int().min(-2147483648).max(2147483647),
int64: () => z$1.int().min(-9007199254740991).max(9007199254740991).meta({ format: "int64" }),
file: (options) => z$1.any().meta({
format: "binary",
...options
}),
stream: () => z$1.any().meta({ format: "stream" }),
valueLabel: () => z$1.object({
value: z.constantCase(),
label: z.text(),
description: z.longText().optional()
}),
/** Pagination wrapper, mirrors `pageSchema(item)`. */
page: (item) => z$1.object({
content: z$1.array(item),
page: z$1.object({
number: z$1.int(),
size: z$1.int(),
offset: z$1.int(),
numberOfElements: z$1.int(),
totalElements: z$1.int().optional(),
totalPages: z$1.int().optional(),
isEmpty: z$1.boolean(),
isFirst: z$1.boolean(),
isLast: z$1.boolean()
})
}),
/** Export a schema to JSON Schema (OpenAPI / form generation). */
toJSONSchema: z$1.toJSONSchema,
/** Runtime type guards (`z.schema.*`) — schema introspection for ORM + forms. */
schema: {
isOptional: (s) => s instanceof z$1.ZodOptional,
isDefault: (s) => s instanceof z$1.ZodDefault,
isObject: (s) => s instanceof z$1.ZodObject,
isString: (s) => defType(s) === "string",
isNumber: (s) => defType(s) === "number",
isBoolean: (s) => s instanceof z$1.ZodBoolean,
isArray: (s) => s instanceof z$1.ZodArray,
isUnion: (s) => s instanceof z$1.ZodUnion,
isNull: (s) => s instanceof z$1.ZodNull,
isRecord: (s) => s instanceof z$1.ZodRecord,
isEnum: (s) => s instanceof z$1.ZodEnum,
isLiteral: (s) => s instanceof z$1.ZodLiteral,
isAny: (s) => s instanceof z$1.ZodAny,
isNullable: (s) => s instanceof z$1.ZodNullable,
isInteger: (s) => defType(s) === "number" && nativeFmt(s) === "safeint",
isText: (s) => defType(s) === "string" && !fmt(s),
isDateTime: (s) => fmt(s) === "date-time",
isDate: (s) => fmt(s) === "date",
isTime: (s) => fmt(s) === "time",
isBigInt: (s) => fmt(s) === "bigint",
isUuid: (s) => fmt(s) === "uuid",
isEmail: (s) => fmt(s) === "email",
isUrl: (s) => fmt(s) === "url",
isBinary: (s) => fmt(s) === "binary",
isUUID: (s) => fmt(s) === "uuid",
isScalar: (s) => s instanceof z$1.ZodString || s instanceof z$1.ZodNumber || s instanceof z$1.ZodBoolean || s instanceof z$1.ZodNull || s instanceof z$1.ZodLiteral || s instanceof z$1.ZodEnum || s instanceof z$1.ZodDate,
isSchema: (s) => s instanceof z$1.ZodType,
isTuple: (s) => s instanceof z$1.ZodTuple,
isUndefined: (s) => s instanceof z$1.ZodUndefined,
isUnsafe: (s) => s instanceof z$1.ZodAny || s instanceof z$1.ZodUnknown,
isVoid: (s) => s instanceof z$1.ZodVoid,
format: (s) => fmt(s),
/** The string values of a `ZodEnum` (typebox-compat for `value.enum`). */
enumValues: (s) => {
const opts = s?.options;
if (Array.isArray(opts)) return opts;
const entries = s?._zod?.def?.entries;
return entries ? Object.values(entries) : [];
},
/** Peel optional / nullable / default wrappers to the inner schema. */
unwrap: (s) => {
let cur = s;
while (cur instanceof z$1.ZodOptional || cur instanceof z$1.ZodNullable || cur instanceof z$1.ZodDefault) cur = typeof cur.unwrap === "function" ? cur.unwrap() : cur?._zod?.def?.innerType;
return cur;
},
/**
* Read a schema's default value (peeling optional/nullable wrappers), or
* `undefined` if there is none. In zod, defaults are a `ZodDefault` wrapper
* (not a `default` data property as in typebox), so `"default" in schema`
* is NOT a valid test — every schema has a `.default()` method.
*/
getDefault: (s) => {
if (s && Object.hasOwn(s, "default")) return s.default;
let cur = s;
while (cur) {
if (cur instanceof z$1.ZodDefault) {
const dv = cur?._zod?.def?.defaultValue;
return typeof dv === "function" ? dv() : dv;
}
if (cur instanceof z$1.ZodOptional || cur instanceof z$1.ZodNullable) {
cur = cur.unwrap();
continue;
}
return;
}
},
/** typebox-compat: the object's required (non-optional) field names. */
requiredKeys: (s) => {
const shape = s?.shape;
return shape ? Object.keys(shape).filter((k) => !(shape[k] instanceof z$1.ZodOptional)) : [];
}
}
};
//#endregion
//#region ../../src/core/providers/TypeProvider.ts
/**
* TypeProvider — now a thin compatibility shim over `z` (zod 4).
*
* typebox is gone. `t` is just `z`, and the legacy `T*` type names are
* re-aliased to their zod equivalents so existing call-sites keep compiling
* while they are migrated to `z` / `Infer`.
*/
/** Raw zod namespace (legacy `Type` escape hatch). */
const Type = z$1;
/** Legacy `TypeProvider` — kept for its static config knobs. */
var TypeProvider = class {
static DEFAULT_STRING_MAX_LENGTH = 255;
static DEFAULT_SHORT_STRING_MAX_LENGTH = 64;
static DEFAULT_LONG_STRING_MAX_LENGTH = 1024;
static DEFAULT_RICH_STRING_MAX_LENGTH = 65535;
static DEFAULT_ARRAY_MAX_ITEMS = 1e3;
static translateError = (error, _locale) => error.message ?? "";
static setLocale = (_locale) => {};
static isValidBigInt = (value) => typeof value === "number" ? Number.isInteger(value) : /^-?\d+$/.test(value.trim());
};
//#endregion
//#region ../../src/core/helpers/coerceStrings.ts
/**
* Coerce a single string value coming from a string-only boundary (HTTP query,
* HTTP headers, environment variables) to the JS type its schema declares.
*
* This is the zod-standard `z.coerce` behavior applied only at the edges where
* inputs are inherently strings — request bodies and the ORM stay strict. A
* value that cannot be coerced is returned unchanged so the subsequent
* validation produces a proper rejection. Arrays coerce element-wise.
*/
const coerceScalar = (schema, value) => {
const base = z.schema.unwrap(schema);
if (z.schema.isArray(base) && Array.isArray(value)) {
const element = base.element;
return value.map((v) => coerceScalar(element, v));
}
if (z.schema.isString(base) && (typeof value === "number" || typeof value === "boolean")) return String(value);
if (typeof value !== "string") return value;
if (z.schema.isInteger(base) || z.schema.isNumber(base)) {
const n = Number(value);
return value.trim() !== "" && !Number.isNaN(n) ? n : value;
}
if (z.schema.isBoolean(base)) {
if (value === "true") return true;
if (value === "false") return false;
return value;
}
return value;
};
/**
* Coerce each declared field of an object against its object schema. Used to
* normalize string-only boundary maps (env vars, query objects) before strict
* validation. Undeclared keys are passed through untouched.
*/
const coerceObject = (schema, value) => {
const shape = schema?.shape;
if (!shape) return value;
const out = { ...value };
for (const key of Object.keys(shape)) if (out[key] != null) out[key] = coerceScalar(shape[key], out[key]);
return out;
};
//#endregion
//#region ../../src/core/errors/MissingContextError.ts
var MissingContextError = class extends AlephaError {
name = "MissingContextError";
constructor() {
super("Missing context. Did you forget to call Alepha.create()?");
this.name = "MissingContextError";
}
};
//#endregion
//#region ../../src/core/helpers/ref.ts
/**
* Store the current context and definition during injection phase.
*
* @internal
*/
const __alephaRef = {};
/**
* Note:
*
* This file is used to share context between $primitives and the Alepha core during the injection phase.
*
* There is no side effect as long as Alepha is not used concurrently in multiple contexts (which is not the case).
*
* // __alephaRef === undefined
* // begin alepha.with()
* // __alephaRef.context = alepha
* // ... injection phase ...
* // __alephaRef.context = undefined
* // end alepha.with()
* // __alephaRef === undefined
*
* As .with() is synchronous, there is no risk of context leakage.
*
* ---------------------------------------------------------------------------------------------------------------------
*
* Why this helper?
*
* It allows to avoid passing Alepha instance to every $hook, $inject, etc. calls. It's a beautiful syntactic sugar.
*
* With sugar:
*
* class A {
* on = $hook( ... ) // <- __alephaRef is set here
* }
*
* Without sugar:
*
* class A {
* constructor(alepha: Alepha) {
* this.on = $hook(alepha, ... ) // <- no need of __alephaRef
* }
* }
*
* One main goal of Alepha is working with classes but without the class verbosity.
* Forcing to pass Alepha instance in constructors would be a step back in that direction!
*
* ---------------------------------------------------------------------------------------------------------------------
*
* TODO: harden the cursor against mid-instantiation throws.
*
* Today, the cleanup of `__alephaRef.alepha`, `__alephaRef.service`, `__alephaRef.parent`
* is performed by the caller after the synchronous instantiation returns. If the
* instantiation throws (e.g., a primitive factory blows up, or a user constructor
* raises), the cursor can be left holding stale references until the next
* instantiation overwrites them. In practice this is harmless because Alepha is
* single-threaded synchronous during boot and the next inject() rewrites the
* cursor — but it is a sharp edge for debuggers (frame inspection shows ghost
* state) and any future tooling that introspects __alephaRef out-of-band.
*
* Plan: wrap each cursor mutation in Alepha.ts in a try/finally that snapshots
* the previous value and restores it on exit (even on throw). Equivalent to a
* lexically-scoped "with" — pushes/pops cleanly. Should be a 5-line refactor in
* Alepha.ts:1131-1132 once we touch it.
*/
//#endregion
//#region ../../src/core/primitives/$context.ts
/**
* Get Alepha instance and current service from the current context.
*
* It can only be used inside $primitive functions.
*
* ```ts
* import { $context } from "alepha";
*
* const $hello = () => {
* const { alepha, service, module } = $context();
*
* // alepha - alepha instance
* // service - class which is creating this primitive, this is NOT the instance but the service definition
* // module - module definition, if any
*
* return {};
* }
*
* class MyService {
* hello = $hello();
* }
*
* const alepha = new Alepha().with(MyService);
* ```
*
* @internal
*/
const $context = () => {
if (!__alephaRef.alepha) throw new MissingContextError();
return {
alepha: __alephaRef.alepha,
service: __alephaRef.service,
module: __alephaRef.service?.[MODULE]
};
};
//#endregion
//#region ../../src/core/helpers/primitive.ts
var Primitive = class {
alepha;
options;
config;
constructor(args) {
this.alepha = args.alepha;
this.options = args.options;
this.config = {
propertyKey: "",
service: args.service,
module: args.module
};
}
/**
* Called automatically by Alepha after the primitive is created.
*/
onInit() {}
/**
* Override (partially) the options of this primitive.
*
* Options are shallow-merged into the existing options — fields not present
* in `partial` are preserved. Must be called BEFORE `alepha.start()`;
* throws once the container has started because options are consumed during
* start-up (route registration, etc.) and later mutations would have no effect.
*
* Typical use case: customize a built-in primitive from a module without
* forking the whole module.
*
* @example Instance-level override
* ```ts
* const auth = alepha.inject(AuthRouter);
* auth.login.override({ lazy: () => import("./MyLogin.tsx") });
* ```
*
* @example Subclass override (constructor body, after super())
* ```ts
* class MyAuthRouter extends AuthRouter {
* constructor() {
* super();
* this.login.override({ lazy: () => import("./MyLogin.tsx") });
* }
* }
* ```
*/
override(partial) {
if (this.alepha.isStarted()) throw new AlephaError(`Cannot override primitive '${this.config.propertyKey}' after Alepha has started. Call override() before alepha.start() / run().`);
Object.assign(this.options, partial);
return this;
}
};
const createPrimitive = (primitive, options) => {
const { alepha, service } = $context();
if (MODULE in primitive && primitive[MODULE]) alepha.with(primitive[MODULE]);
return alepha.inject(primitive, {
lifetime: "transient",
args: [{
options,
alepha,
service: service ?? Alepha
}]
});
};
//#endregion
//#region ../../src/core/interfaces/Service.ts
function isClass(func) {
if (typeof func !== "function") return false;
const descriptor = Object.getOwnPropertyDescriptor(func, "prototype");
return !!descriptor && !descriptor.writable;
}
//#endregion
//#region ../../src/core/primitives/$module.ts
/**
* Wrap Services and Primitives into a Module.
*
* - A module is just a Service with some extra {@link Module}.
* - You must attach a `name` to it.
* - Name must follow the pattern: `project.module.submodule`. (e.g. `myapp.users.auth`).
*
* @example
* ```ts
* import { $module } from "alepha";
* import { MyService } from "./MyService.ts";
*
* export default $module({
* name: "my.project.module",
* services: [MyService],
* });
* ```
*
* ### Slots
*
* - `services[]` — always auto-injected. Module metadata attached.
* - `variants[]` — module metadata attached but NOT auto-injected. Two typical uses:
* (1) alternative implementations picked at register-time via `alepha.with({ provide, use })`;
* (2) services whose instantiation is driven externally (e.g., the framework core).
* - `imports[]` — other modules this one depends on. Wired before `register()` runs.
* - `atoms[]` — registered on the store.
* - `primitives[]` — tagged with module metadata.
* - `register(alepha)` — purely additive side-effect hook. Runs AFTER `imports[]`
* are wired and BEFORE `services[]` are auto-injected — so substitutions it
* records (e.g. `alepha.with({ provide, use })`) apply to the subsequent
* auto-injection. It can never suppress auto-registration.
*
* ### Why Modules?
*
* #### Logging
*
* By default, AlephaLogger will log the module name in the logs.
* This helps to identify where the logs are coming from.
*
* You can also set different log levels for different modules.
*
* #### Modulith
*
* Force to structure your application in modules, even if it's a single deployable unit.
* It helps to keep a clean architecture and avoid monolithic applications.
*
* ### When not to use Modules?
*
* Small applications do not need modules. Modules earn their keep when the application
* grows — as a rule of thumb, once a module has 30+ `$actions`, consider splitting it.
*/
const $module = (options) => {
const { services = [], variants = [], imports = [], primitives = [], name } = options;
if (!name || !Module.NAME_REGEX.test(name)) throw new AlephaError(`Invalid module name '${name}'. It should be in the format of 'project.module.submodule'`);
const $ = class extends Module {
options = options;
register(alepha) {
if (options.atoms) for (const atom of options.atoms) alepha.store.register(atom);
options.register?.(alepha);
for (const mod of imports) alepha.with(mod);
for (const service of services) alepha.inject(service, { parent: this.constructor });
}
};
Object.defineProperty($, "name", {
value: name,
writable: false
});
for (const service of [...services, ...variants]) if (!Module.is(service)) service[MODULE] = $;
for (const factory of primitives) if (typeof factory[KIND] === "function") factory[KIND][MODULE] = $;
return $;
};
/**
* Base class for all modules.
*/
var Module = class Module {
static NAME_REGEX = /^[a-z-]+(\.[a-z-][a-z0-9-]*)*$/;
/**
* Check if a Service is a Module.
*/
static is(ctor) {
return ctor.prototype instanceof Module;
}
/**
* Get the Module of a Service.
*
* Returns undefined if the Service is not part of a Module.
*/
static of(ctor) {
return ctor[MODULE];
}
};
//#endregion
//#region ../../src/core/providers/AlsProvider.ts
const ALS_PARENT = Symbol("als.parent");
var AlsProvider = class AlsProvider {
static create = () => {};
als;
constructor() {
this.als = AlsProvider.create();
}
createContextId() {
return crypto.randomUUID();
}
run(callback, data = {}) {
if (!this.als) return callback();
const parent = this.als.getStore() ?? void 0;
data.registry ??= /* @__PURE__ */ new Map();
data.context ??= this.createContextId();
return this.als.run({
...data,
[ALS_PARENT]: parent
}, callback);
}
exists() {
return !!this.get("context");
}
get(key, scope) {
if (!this.als) return;
const store = this.als.getStore();
if (!store) return;
if (scope === "app") return;
if (scope === "current") return key in store ? store[key] : void 0;
if (scope === "parent") return store[ALS_PARENT]?.[key];
if (key in store) return store[key];
let current = store[ALS_PARENT];
while (current) {
if (key in current) return current[key];
current = current[ALS_PARENT];
}
}
/**
* Return the raw ALS layer object that owns `key`, walking from the
* current layer up through parent forks — the same resolution order as
* the default-scope branch of {@link get}. Returns `undefined` when no
* active ALS layer (current or any ancestor) has the key.
*
* Used by `StateManager.register()` to decode a value in place, in
* whichever fork layer it physically lives, instead of flattening
* fork-scoped state into the app-level store.
*/
getLayer(key) {
if (!this.als) return;
let current = this.als.getStore();
while (current) {
if (key in current) return current;
current = current[ALS_PARENT];
}
}
has(key) {
if (!this.als) return false;
const store = this.als.getStore();
if (!store) return false;
if (key in store) return true;
let current = store[ALS_PARENT];
while (current) {
if (key in current) return true;
current = current[ALS_PARENT];
}
return false;
}
set(key, value) {
if (!this.als) return;
const store = this.als.getStore();
if (store) store[key] = value;
}
};
//#endregion
//#region ../../src/core/primitives/$inject.ts
/**
* Get the instance of the specified type from the context.
*
* ```ts
* class A { }
* class B {
* a = $inject(A);
* }
* ```
*/
const $inject = (type, opts = {}) => {
const { alepha, service } = $context();
if (type === alepha.constructor) return alepha;
return alepha.inject(type, {
parent: service ?? alepha.constructor,
...opts
});
};
var InjectPrimitive = class extends Primitive {};
//#endregion
//#region ../../src/core/providers/Json.ts
/**
* Mimics the JSON global object with stringify and parse methods.
*
* Used across the codebase via dependency injection.
*/
var Json = class {
stringify = JSON.stringify;
parse = JSON.parse;
};
//#endregion
//#region ../../src/core/providers/SchemaCodec.ts
var SchemaCodec = class {};
//#endregion
//#region ../../src/core/providers/JsonSchemaCodec.ts
var JsonSchemaCodec = class extends SchemaCodec {
json = $inject(Json);
encoder = new TextEncoder();
decoder = new TextDecoder();
encodeToString(schema, value) {
return this.json.stringify(value);
}
encodeToBinary(schema, value) {
return this.encoder.encode(this.encodeToString(schema, value));
}
decode(schema, value) {
if (value instanceof Uint8Array) {
const text = this.decoder.decode(value);
return this.json.parse(text);
}
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return this.json.parse(value);
return value;
}
return value;
}
};
//#endregion
//#region ../../src/core/primitives/$hook.ts
/**
* Registers a new hook.
*
* ```ts
* import { $hook } from "alepha";
*
* class MyProvider {
* onStart = $hook({
* name: "start", // or "configure", "ready", "stop", ...
* handler: async (app) => {
* // await db.connect(); ...
* }
* });
* }
* ```
*
* Hooks are used to run async functions from all registered providers/services.
*
* You can't register a hook after the App has started.
*
* It's used under the hood by the `configure`, `start`, and `stop` methods.
* Some modules also use hooks to run their own logic. (e.g. `alepha/server`).
*
* You can create your own hooks by using module augmentation:
*
* ```ts
* declare module "alepha" {
*
* interface Hooks {
* "my:custom:hook": {
* arg1: string;
* }
* }
* }
*
* await alepha.events.emit("my:custom:hook", { arg1: "value" });
* ```
*
*/
const $hook = (options) => createPrimitive(HookPrimitive, options);
var HookPrimitive = class extends Primitive {
called = 0;
onInit() {
const handler = this.options.handler;
const resolveDeps = (deps) => {
if (!deps) return void 0;
return (Array.isArray(deps) ? deps : [deps]).map((dep) => dep.constructor);
};
this.alepha.events.on(this.options.on, {
caller: this.config.service,
priority: this.options.priority,
before: resolveDeps(this.options.before),
after: resolveDeps(this.options.after),
callback: (args) => {
this.called += 1;
return handler(args);
}
});
}
};
$hook[KIND] = HookPrimitive;
//#endregion
//#region ../../src/core/providers/KeylessJsonSchemaCodec.ts
/**
* KeylessJsonSchemaCodec provides schema-driven JSON encoding without keys.
*
* It uses the schema to determine field order, allowing the encoded output
* to be a simple JSON array instead of an object with keys.
*/
var KeylessJsonSchemaCodec = class extends SchemaCodec {
cache = /* @__PURE__ */ new Map();
textEncoder = new TextEncoder();
textDecoder = new TextDecoder();
varCounter = 0;
useFunctionCompilation = true;
/**
* Configure codec options.
*/
configure(options) {
if (options.useFunctionCompilation !== void 0) {
this.useFunctionCompilation = options.useFunctionCompilation;
this.cache.clear();
}
return this;
}
/**
* Hook to auto-detect safe mode on configure.
* Disables function compilation in browser by default.
*/
onConfigure = $hook({
on: "configure",
handler: () => {
this.useFunctionCompilation = this.canUseFunction();
}
});
/**
* Encode value to a keyless JSON string.
*/
encodeToString(schema, value) {
return this.getCodec(schema).encode(value);
}
/**
* Encode value to binary (UTF-8 encoded keyless JSON).
*/
encodeToBinary(schema, value) {
return this.textEncoder.encode(this.encodeToString(schema, value));
}
/**
* Decode keyless JSON string or binary to value.
*/
decode(schema, value) {
if (value instanceof Uint8Array) {
const text = this.textDecoder.decode(value);
return this.getCodec(schema).decode(text);
}
if (typeof value === "string") return this.getCodec(schema).decode(value);
if (Array.isArray(value)) return this.reconstructObject(schema, value);
return value;
}
/**
* Test if `new Function()` is available (not blocked by CSP).
*/
canUseFunction() {
try {
return new Function("return true")() === true;
} catch {
return false;
}
}
/**
* Get a compiled codec for the given schema.
* Codecs are cached for reuse.
*/
getCodec(schema) {
let c = this.cache.get(schema);
if (!c) {
c = this.useFunctionCompilation ? this.compileWithFunction(schema) : this.compileInterpreted(schema);
this.cache.set(schema, c);
}
return c;
}
nextVar() {
return `_${this.varCounter++}`;
}
/**
* Compile codec using `new Function()` for maximum performance.
* Only used when CSP allows and useFunctionCompilation is true.
*/
compileWithFunction(schema) {
this.varCounter = 0;
const encBody = this.genEnc(schema, "v");
this.varCounter = 0;
const decBody = this.genDec(schema);
return {
encode: new Function("v", `return JSON.stringify(${encBody});`),
decode: new Function("s", `const a=JSON.parse(s);let i=0;${decBody.code}return ${decBody.result};`)
};
}
/**
* Compile codec using interpreter-based approach.
* Safer (no eval/Function) but slower. Used in browser by default.
*/
compileInterpreted(schema) {
const self = this;
return {
encode(value) {
return JSON.stringify(self.interpretEncode(schema, value));
},
decode(str) {
const ctx = {
arr: JSON.parse(str),
i: 0
};
return self.interpretDecode(schema, ctx);
}
};
}
interpretEncode(schema, value) {
if (this.isLeaf(schema)) return value;
if (z.schema.isBigInt(schema)) return `${value}n`;
if (z.schema.isArray(schema)) {
const arrSchema = schema;
if (!Array.isArray(value)) return value;
if (z.schema.isScalar(arrSchema.items)) return value;
return value.map((e) => this.interpretEncode(arrSchema.items, e));
}
if (z.schema.isObject(schema)) {
const result = [];
for (const { key, isOpt, isNullable, inner } of this.getObjectFields(schema)) {
const v = value[key];
if (isOpt) result.push(v !== void 0 ? this.interpretEncode(inner, v) : null);
else if (isNullable) result.push(v !== null ? this.interpretEncode(inner, v) : null);
else result.push(this.interpretEncode(inner, v));
}
return result;
}
if (z.schema.isOptional(schema) || z.schema.isUnion(schema)) {
const inner = this.unwrap(schema);
if (this.isNullable(schema)) return value !== null ? this.interpretEncode(inner, value) : null;
return value !== void 0 ? this.interpretEncode(inner, value) : null;
}
return value;
}
interpretDecode(schema, ctx) {
if (this.isLeaf(schema)) return ctx.arr[ctx.i++];
if (z.schema.isBigInt(schema)) return BigInt(ctx.arr[ctx.i++].slice(0, -1));
if (z.schema.isArray(schema)) {
const arrSchema = schema;
const arr = ctx.arr[ctx.i++];
if (!Array.isArray(arr)) return arr;
if (z.schema.isObject(arrSchema.items)) return arr.map((e) => this.interpretDecodeFromValue(arrSchema.items, e));
return arr;
}
if (z.schema.isObject(schema)) {
const result = {};
for (const { key, isOpt, isNullable, inner } of this.getObjectFields(schema)) {
const val = ctx.arr[ctx.i++];
if (isOpt) {
if (val !== null) result[key] = this.interpretDecodeFromValue(inner, val);
} else if (isNullable) result[key] = val === null ? null : this.interpretDecodeFromValue(inner, val);
else result[key] = this.interpretDecodeFromValue(inner, val);
}
return result;
}
if (z.schema.isOptional(schema) || z.schema.isUnion(schema)) {
const inner = this.unwrap(schema);
const val = ctx.arr[ctx.i++];
if (val === null) return this.isNullable(schema) ? null : void 0;
if (z.schema.isObject(inner) || z.schema.isArray(inner)) return this.interpretDecodeFromValue(inner, val);
return val;
}
return ctx.arr[ctx.i++];
}
interpretDecodeFromValue(schema, value) {
if (this.isLeaf(schema)) return value;
if (z.schema.isBigInt(schema)) return BigInt(value.slice(0, -1));
if (z.schema.isArray(schema)) {
if (!Array.isArray(value)) return value;
const arrSchema = schema;
if (z.schema.isObject(arrSchema.items)) return value.map((e) => this.interpretDecodeFromValue(arrSchema.items, e));
return value;
}
if (z.schema.isObject(schema)) {
const props = schema.properties;
const keys = Object.keys(props);
const result = {};
for (let idx = 0; idx < keys.length; idx++) {
const k = keys[idx];
const prop = props[k];
const isOpt = z.schema.isOptional(prop);
const inner = this.unwrap(prop);
const v = value[idx];
if (isOpt && v === null) continue;
if (z.schema.isObject(inner)) result[k] = this.interpretDecodeFromValue(inner, v);
else if (z.schema.isBigInt(inner)) result[k] = BigInt(v.slice(0, -1));
else result[k] = v;
}
return result;
}
return value;
}
genEnc(schema, ve) {
if (this.isLeaf(schema)) return ve;
if (z.schema.isBigInt(schema)) return `${ve}+'n'`;
if (z.schema.isArray(schema)) {
const arrSchema = schema;
const itemEnc = this.genEnc(arrSchema.items, "e");
if (z.schema.isScalar(arrSchema.items)) return ve;
return `${ve}.map(e=>${itemEnc})`;
}
if (z.schema.isObject(schema)) {
const parts = [];
for (const { key, isOpt, isNullable, inner } of this.getObjectFields(schema)) {
const access = `${ve}[${JSON.stringify(key)}]`;
const innerEnc = this.genEnc(inner, access);
if (isOpt) parts.push(`${access}!==undefined?${innerEnc}:null`);
else if (isNullable) parts.push(`${access}!==null?${innerEnc}:null`);
else parts.push(innerEnc);
}
return `[${parts.join(",")}]`;
}
if (z.schema.isOptional(schema) || z.schema.isUnion(schema)) {
const inner = this.unwrap(schema);
const innerEnc = this.genEnc(inner, ve);
if (this.isNullable(schema)) return `${ve}!==null?${innerEnc}:null`;
return `${ve}!==undefined?${innerEnc}:null`;
}
return ve;
}
genDec(schema) {
const v = this.nextVar();
if (this.isLeaf(schema)) return {
code: "",
result: "a[i++]"
};
if (z.schema.isBigInt(schema)) return {
code: "",
result: "BigInt(a[i++].slice(0,-1))"
};
if (z.schema.isArray(schema)) {
const arrSchema = schema;
if (z.schema.isObject(arrSchema.items)) return {
code: "",
result: `a[i++].map(e=>${this.genDecFromValue(arrSchema.items, "e")})`
};
return {
code: "",
result: "a[i++]"
};
}
if (z.schema.isObject(schema)) {
const fields = this.getObjectFields(schema);
if (fields.every(({ isOpt, isNullable, inner }) => !isOpt && !isNullable && !z.schema.isObject(inner) && !z.schema.isArray(inner))) return {
code: "",
result: `{${fields.map(({ key }) => `${JSON.stringify(key)}:a[i++]`).join(",")}}`
};
let code = `const ${v}={};`;
for (const { key, isOpt, isNullable, inner } of fields) {
const sk = JSON.stringify(key);
if (isOpt) {
const nested = this.genDecFromValue(inner, "t");
code += `{const t=a[i++];if(t!==null){${v}[${sk}]=${nested};}}`;
} else if (isNullable) {
const nested = this.genDecFromValue(inner, "t");
code += `{const t=a[i++];if(t===null){${v}[${sk}]=null;}else{${v}[${sk}]=${nested};}}`;
} else if (z.schema.isObject(inner)) {
const nested = this.genDecFromValue(inner, "a[i++]");
code += `${v}[${sk}]=${nested};`;
} else if (z.schema.isArray(inner)) {
const arrSchema = inner;
if (z.schema.isObject(arrSchema.items)) {
const itemTransform = this.genDecFromValue(arrSchema.items, "e");
code += `${v}[${sk}]=a[i++].map(e=>${itemTransform});`;
} else code += `${v}[${sk}]=a[i++];`;
} else code += `${v}[${sk}]=a[i++];`;
}
return {
code,
result: v
};
}
if (z.schema.isOptional(schema) || z.schema.isUnion(schema)) {
const inner = this.unwrap(schema);
const innerDec = this.genDec(inner);
return {
code: `const ${v}t=a[i++];let ${v};if(${v}t===null){${v}=${this.isNullable(schema) ? "null" : "undefined"};}else{${innerDec.code.replace(/a\[i\+\+\]/g, `${v}t`)}${v}=${innerDec.result.replace(/a\[i\+\+\]/g, `${v}t`)};}`,
result: v
};
}
return {
code: "",
result: "a[i++]"
};
}
genDecFromValue(schema, expr) {
if (this.isLeaf(schema)) return expr;
if (z.schema.isBigInt(schema)) return `BigInt(${expr}.slice(0,-1))`;
if (z.schema.isArray(schema)) {
const items = schema.items;
if (z.schema.isObject(items) || z.schema.isBigInt(items)) {
const v = this.nextVar();
return `${expr}.map(${v}=>${this.genDecFromValue(items, v)})`;
}
return expr;
}
if (z.schema.isObject(schema)) {
const props = schema.properties;
const keys = Object.keys(props);
const v = this.nextVar();
return `((${v}=${expr})=>({${keys.map((k, idx) => {
const inner = this.unwrap(props[k]);
const innerExpr = `${v}[${idx}]`;
const sk = JSON.stringify(k);
if (z.schema.isObject(inner)) return `${sk}:${this.genDecFromValue(inner, innerExpr)}`;
if (z.schema.isBigInt(inner)) return `${sk}:BigInt(${innerExpr}.slice(0,-1))`;
return `${sk}:${innerExpr}`;
}).join(",")}}))()`;
}
return expr;
}
isLeaf(schema) {
return z.schema.isScalar(schema) || this.isEnum(schema);
}
getObjectFields(schema) {
const props = schema.properties;
const req = new Set(z.schema.requiredKeys(schema));
return Object.keys(props).map((key) => {
const ps = props[key];
return {
key,
isOpt: !req.has(key) || z.schema.isOptional(ps),
isNullable: this.isNullable(ps),
inner: this.unwrap(ps)
};
});
}
isEnum(schema) {
return "enum" in schema && Array.isArray(schema.enum) && (schema.enum?.length ?? 0) > 0;
}
isNullable(schema) {
let cur = schema;
while (cur) {
if (z.schema.isNullable(cur)) return true;
if (z.schema.isOptional(cur) && typeof cur.unwrap === "function") {
cur = cur.unwrap();
continue;
}
break;
}
if (z.schema.isUnion(schema)) return schema.anyOf?.some((s) => z.schema.isNull(s)) ?? false;
return false;
}
unwrap(schema) {
const peeled = z.schema.unwrap(schema);
if (z.schema.isUnion(peeled)) {
const unionSchema = peeled;
if (Array.isArray(unionSchema.anyOf)) return unionSchema.anyOf.find((s) => !z.schema.isNull(s)) || peeled;
}
return peeled;
}
/**
* Reconstruct an object from a parsed array (for when input is already parsed).
*/
reconstructObject(schema, arr) {
if (!z.schema.isObject(schema)) return arr;
const result = {};
let i = 0;
for (const { key, isOpt, isNullable, inner } of this.getObjectFields(schema)) {
const val = arr[i++];
if (isOpt) {
if (val !== null) result[key] = z.schema.isObject(inner) ? this.reconstructObject(inner, val) : val;
} else if (isNullable) result[key] = val === null ? null : z.schema.isObject(inner) ? this.reconstructObject(inner, val) : val;
else result[key] = z.schema.isObject(inner) ? this.reconstructObject(inner, val) : val;
}
return result;
}
};
//#endregion
//#region ../../src/core/errors/TypeBoxError.ts
var TypeBoxError = class extends AlephaError {
name = "TypeBoxError";
cause;
value;
constructor(error) {
super(`Invalid input: ${error.message}${error.instancePath ? ` at ${error.instancePath}` : ""}`, { cause: error });
const params = error.params;
if (params?.requiredProperties) this.value = {
path: `/${params.requiredProperties[0]}`,
message: "must be defined"
};
else this.value = {
path: error.instancePath ?? "",
message: error.message
};
this.cause = error;
}
};
//#endregion
//#region ../../src/core/providers/SchemaValidator.ts
/**
* Validates + coerces a value against a zod schema.
*
* Trimming / lowercasing / defaults / unknown-key stripping all live in the
* schema itself now (zod), so this is a thin wrapper over `schema.parse`.
* No typebox `Compile`, no `eval` — safe inside Cloudflare Workers.
*/
var SchemaValidator = class {
validate(schema, value, _options = {}) {
const result = schema.safeParse(value);
if (!result.success) {
const issue = result.error.issues[0];
const path = issue?.path?.length ? issue.path.join("/") : "";
let message = issue?.message ?? "Validation failed";
if (issue?.code === "invalid_type" && /received undefined/i.test(message) && path) message = `'${path}' is required`;
throw new TypeBoxError({
message,
instancePath: path ? `/${path}` : "",
params: issue
});
}
return result.data;
}
safeValidate(schema, value) {
return schema.safeParse(value);
}
/** Zod schemas are immutable — clone is a pass-through. */
clone(schema) {
return schema;
}
/**
* Legacy pre-processing entry point. Coercion now happens inside `parse`,
* so this is a no-op kept for call-site compatibility.
*/
beforeParse(_schema, value, _options) {
return value;
}
};
//#endregion
//#region ../../src/core/providers/CodecManager.ts
/**
* CodecManager manages multiple codec formats and provides a unified interface
* for encoding and decoding data with different formats.
*/
var CodecManager = class {
codecs = /* @__PURE__ */ new Map();
jsonCodec = $inject(JsonSchemaCodec);
keylessCodec = $inject(KeylessJsonSchemaCodec);
schemaValidator = $inject(SchemaValidator);
default = "json";
constructor() {
this.register({
name: "json",
codec: this.jsonCodec,
default: true
});
this.register({
name: "keyless",
codec: this.keylessCodec
});
}
/**
* Register a new codec format.
*/
register(opts) {
this.codecs.set(opts.name, opts.codec);
if (opts.default) this.default = opts.name;
}
/**
* Get a specific codec by name.
*
* @param name - The name of the codec
* @returns The codec instance
* @throws {AlephaError} If the codec is not found
*/
getCodec(name) {
const codec = this.codecs.get(name);
if (!codec) throw new AlephaError(`Codec "${name}" not found. Available codecs: ${Array.from(this.codecs.keys()).join(", ")}`);
return codec;
}
/**
* Encode data using the specified codec and output format.
*/
encode(schema, value, options) {
const codec = this.getCodec(options?.encoder ?? this.default);
const as = options?.as ?? "object";
if (options?.validation !== false) value = this.schemaValidator.validate(schema, value, options?.validation);
if (as === "object") return value;
if (as === "binary") return codec.encodeToBinary(schema, value);
return codec.encodeToString(schema, value);
}
/**
* Decode data using the specified codec.
*/
decode(schema, data, options) {
const encoderName = options?.encoder ?? this.default;
let value = this.getCodec(encoderName).decode(schema, data);
if (options?.validation !== false) value = this.schemaValidator.validate(schema, value, options?.validation);
return value;
}
/**
* Validate decoded data against the schema.
*
* This is automatically called before encoding or after decoding.
*/
validate(schema, value, options) {
return this.schemaValidator.validate(schema, value, options);
}
};
//#endregion
//#region ../../src/core/providers/EventManager.ts
var EventManager = class {
logFn;
/**
* Three-list storage for hooks, split by priority tier.
*/
events = {};
/**
* Cache of compiled executors, auto-built on first emit per event.
*/
cache = /* @__PURE__ */ new Map();
/**
* Cache of resolved (topo-sorted) hook arrays per event.
*/
sortedCache = /* @__PURE__ */ new Map();
constructor(logFn) {
this.logFn = logFn;
}
get log() {
return this.logFn?.();
}
clear() {
this.events = {};
this.cache.clear();
this.sortedCache.clear();
}
/**
* Registers a hook for the specified event.
*/
on(event, hookOrFunc) {
if (!this.events[event]) this.events[event] = {
first: [],
normal: [],
last: []
};
const hook = typeof hookOrFunc === "function" ? { callback: hookOrFunc } : hookOrFunc;
const lists = this.events[event];
if (hook.priority === "first") lists.first.push(hook);
else if (hook.priority === "last") lists.last.push(hook);
else lists.normal.push(hook);
this.invalidateCache(event);
return () => {
const lists = this.events[event];
if (!lists) return;
for (const key of [
"first",
"normal",
"last"
]) lists[key] = lists[key].filter((it) => it.callback !== hook.callback);
this.invalidateCache(event);
};
}
invalidateCache(event) {
this.cache.delete(event);
this.cache.delete(`${event}:catch`);
this.sortedCache.delete(event);
}
/**
* Resolves the final ordered hook list for an event.
* Applies topological sort (Kahn's algorithm) within each priority tier
* based on before/after constraints.
*/
resolve(event) {
const cached = this.sortedCache.get(event);
if (cached) return cached;
const lists = this.events[event];
if (!lists) return [];
const sorted = [
...this.topoSort(lists.first),
...this.topoSort(lists.normal),
...this.topoSort(lists.last)
];
this.sortedCache.set(event, sorted);
return sorted;
}
/**
* Topological sort using Kahn's algorithm.
* Hooks with no constraints maintain registration order (stable).
*/
topoSort(hooks) {
if (hooks.length <= 1) return hooks;
if (!hooks.some((h) => h.before && h.before.length > 0 || h.after && h.after.length > 0)) return hooks;
const inDegree = /* @__PURE__ */ new Map();
const adjacency = /* @__PURE__ */ new Map();
for (const hook of hooks) {
inDegree.set(hook, 0);
adjacency.set(hook, /* @__PURE__ */ new Set());
}
const serviceToHooks = /* @__PURE__ */ new Map();
for (const hook of hooks) if (hook.caller) {
let arr = serviceToHooks.get(hook.caller);
if (!arr) {
arr = [];
serviceToHooks.set(hook.caller, arr);
}
arr.push(hook);
}
for (const hook of hooks) {
if (hook.after) for (const dep of hook.after) {
const depHooks = serviceToHooks.get(dep);
if (depHooks) {
for (const depHook of depHooks) if (depHook !== hook && !adjacency.get(depHook).has(hook)) {
adjacency.get(depHook).add(hook);
inDegree.set(hook, inDegree.get(hook) + 1);
}
}
}
if (hook.before) for (const dep of hook.before) {
const depHooks = serviceToHooks.get(dep);
if (depHooks) {
for (const depHook of depHooks) if (depHook !== hook && !adjacency.get(hook).has(depHook)) {
adjacency.get(hook).add(depHook);
inDegree.set(depHook, inDegree.get(depHook) + 1);
}
}
}
}
const queue = [];
for (const hook of hooks) if (inDegree.get(hook) === 0) queue.push(hook);
const result = [];
while (queue.length > 0) {
const hook = queue.shift();
result.push(hook);
for (const neighbor of adjacency.get(hook)) {
const deg = inDegree.get(neighbor) - 1;
inDegree.set(neighbor, deg);
if (deg === 0) queue.push(neighbor);
}
}
if (result.length !== hooks.length) throw new AlephaError("Circular dependency detected in hook before/after constraints");
return result;
}
/**
* Compiles an event into an optimized executor function.
*
* Called automatically by emit() on first use. Can also be called
* manually for direct access to the executor.
*/
compile(event, options = {}) {
const hooks = this.resolve(event);
if (hooks.length === 0) return () => {};
const catchErrors = options.catch ?? false;
const log = this.log;
const runRemainingAsync = async (startIndex, payload) => {
for (let i = startIndex; i < hooks.length; i++) {
const hook = hooks[i];
try {
const result = hook.callback(payload);
if (result && typeof result === "object" && "then" in result) if (catchErrors) await result.catch((error) => {
log?.error(`${String(event)}(${hook.caller?.name ?? "unknown"}) ERROR`, error);
});
else await result;
} catch (error) {
if (catchErrors) log?.error(`${String(event)}(${hook.caller?.name ?? "unknown"}) ERROR`, error);
else throw error;
}
}
};
return (payload) => {
for (let i = 0; i < hooks.length; i++) {
const hook = hooks[i];
try {
const result = hook.callback(payload);
if (result && typeof result === "object" && "then" in result) {
if (catchErrors) return result.catch((error) => {
log?.error(`${String(event)}(${hook.caller?.name ?? "unknown"}) ERROR`, error);
}).then(() => runRemainingAsync(i + 1, payload));
return result.then(() => runRemainingAsync(i + 1, payload));
}
} catch (error) {
if (catchErrors) log?.error(`${String(event)}(${hook.caller?.name ?? "unknown"}) ERROR`, error);
else throw error;
}
}
};
}
/**
* Emits the specified event with the given payload.
*
* Auto-compiles and caches an optimized executor on first call per event.
* Use `{ log: true }` for startup events that need timing information.
*/
async emit(event, payload, options = {}) {
if (!options.log) {
const cacheKey = options.catch ? `${event}:catch` : event;
let executor = this.cache.get(cacheKey);
if (!executor) {
executor = this.compile(event, { catch: options.catch });
this.cache.set(cacheKey, executor);
}
await executor(payload);
return;
}
const events = this.resolve(event);
if (events.length === 0) return;
const now = performance.now();
this.log?.trace(`${String(event)} ...`);
for (const hook of events) {
const name = hook.caller?.name ?? "unknown";
const hookStart = performance.now();
this.log?.trace(`${String(event)}(${name}) ...`);
try {
const result = hook.callback(payload);
if (result && typeof result === "object" && "then" in result) aw