UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

1,556 lines 110 kB
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) await result; } catch (error) { if (options.catch