Перейти к содержимому

@aom-js/core

Механика декораторной фабрики aom: реестр мета-инстансов, узел, фазы исполнения, наследование метаданных, resolveFor. Документ — единый источник истины по этой механике; router, schemer, transfer, holder и api ссылаются сюда вместо собственных объяснений. Читать до: Архитектура ядра (состав ядра, граф пакетов, сборка). Читать после: @aom-js/router, @aom-js/schemer.

Пакет даёт любому мета-классу — наследнику AomCore — две вещи: собственный реестр «мета-инстансов», ключуемых парой «класс + свойство», и пять фабрик legacy-декораторов, которые не выполняют обработчик немедленно, а кладут ленивый вызов [handler, [$, ...args], inherit, parameter?] в очередь одной из четырёх фаз (ExecutionPhase); четвёртый элемент — признак вызова декоратора параметра конструктора, используется при переносе на потомка (см. §«Наследование…»). Всё прикладное состояние хранят поля наследника; сам AomCore держит единственное поле экземпляра — узел _ (packages/core/src/defines/core.ts, AomCore._).

Runtime-зависимостей у пакета нет (packages/core/package.json, dependencies: {}); reflect-metadata он не использует и о HTTP ничего не знает. Потребители — все остальные пакеты ядра: router (AomApi, packages/router/src/common/api.ts), schemer (AomSchemas, packages/schemer/src/defines/module.ts), transfer (AomIsolator, packages/transfer/src/defines/main.ts), holder (AomHolder, packages/holder/src/init.ts), api (AomAccessCore, packages/api/src/controllers/access/types.ts). Приложение на ядре может объявить и собственный мета-класс (см. §«Пример собственного мета-класса»). Полный граф пакетов и порядок сборки — Архитектура ядра.

Точка входа — packages/core/src/index.ts → export * from "./defines"; каталога констант в packages/core/src нет.

ЭкспортСигнатураНазначение
AomCoreclass AomCore { _: AomNode<this>; constructor(base, target, property?, origin?) }Базовый мета-класс; наследуется и получает собственный реестр и фабрики
AomCore.instancestatic instance<T, E>(this: AomClass<T>, target: E, property?: Property): TНайти или создать мета-инстанс по target (+property)
AomCore.checkExistsstatic checkExists<T, E>(this: AomClass<T>, target: E, property?: Property): T | undefinedВернуть существующий инстанс без создания; для свойства без дескриптора может создать синтетический инстанс (см. §«Ограничения»)
AomCore.resolveForstatic resolveFor<T>(this: AomClass<T>, $: T, context: ClassConstructor): TПерепривязать инстанс статики родителя к каррированной копии потомка
AomCore.classDecoratorstatic classDecorator<T>(handler: ($: T) => void, storeFlag?: ExecutionPhase, inherit?: InheritPlacement): ClassDecoratorФабрика декоратора класса
AomCore.methodDecoratorstatic methodDecorator<T>(handler: ($: T, descriptor: PropertyDescriptor) => void, storeFlag?, inherit?): MethodDecoratorФабрика декоратора метода (статического или прототипного); обработчик возвращает void: возврат всё равно никуда не доходит
AomCore.propertyDecoratorstatic propertyDecorator<T>(handler: ($: T) => void, storeFlag?, inherit?): PropertyDecoratorФабрика декоратора поля
AomCore.parameterDecoratorstatic parameterDecorator<T>(handler: ($: T, parameterIndex: number) => void, storeFlag?, inherit?): ParameterDecoratorФабрика декоратора параметра; parameterIndex передаётся только обработчику
AomCore.combinedDecoratorstatic combinedDecorator<T>(handler: ($: T, descriptor?: PropertyDescriptor) => void, storeFlag?, inherit?): CombinedDecoratorДекоратор для класса ИЛИ метода; descriptor есть только у метода
ClassInstanceMapclass ClassInstanceMap { set<T>(key: ClassConstructor<T>, value: T): this; get<T>(key): T | undefined; has(key): boolean; delete(key): boolean }Типизированная WeakMap «конструктор → экземпляр»; в самом core не используется, нужна router как RxContext.state
isClassfunction isClass(value: InstanceTarget): booleanПроверка «это class-синтаксис» через Function.prototype.toString

Значения по умолчанию у всех пяти фабрик: storeFlag = ExecutionPhase.FIRST, inherit не задан (packages/core/src/defines/core.ts, classDecorator, combinedDecorator, methodDecorator, propertyDecorator, parameterDecorator).

ЭкспортСигнатураНазначение
ExecutionPhaseenum { INSTANT="instant", FIRST="first", LAST="last", DELAY="delay" }Фаза выполнения ленивого вызова (packages/core/src/defines/types.ts)
InheritPlacementenum { APPEND="append", PREPEND="prepend", SKIP="skip" }Размещение вызова при переносе на потомка (packages/core/src/defines/types.ts)
ClassConstructortype ClassConstructor<T = {}> = new (...args: any[]) => TТип конструктора
ClassTypetype ClassType = ClassConstructor | FunctionКонструктор или функция
Propertytype Property = string | symbolКлюч свойства
InstanceTargettype InstanceTarget = ClassType | InstanceType<ClassConstructor>Цель декорирования: класс, функция, прототип или объект
CombinedDecoratortype CombinedDecorator = <T>(target: ClassType, propertyKey?: Property, descriptor?: TypedPropertyDescriptor<T>) => voidСигнатура комбинированного декоратора
СимволГдеПочему важен
AomNodepackages/core/src/defines/node.tsМодель узла; доступна только через $._ — тип наружу не выведен
AomCallstack, AomCallstackQueuepackages/core/src/defines/callstack.tsОчереди фаз; доступны через $._.callstack
LazyCall, OriginDescriptor, NodeOriginDescriptor, AomClasscallstack.ts (LazyCall), node.ts (NodeOriginDescriptor), internals.ts (OriginDescriptor, AomClass)Внутренние типы ленивого вызова и origin-дескриптора

Собственных констант ошибок в пакете нет; router держит свои (packages/router/src/constants/errors.ts, CONSTRUCTOR_TYPE_ERROR, CONSTRUCTOR_PROPERTY_TYPE_ERROR).

ФайлОтветственностьКлючевые символы
packages/core/src/index.tsЕдинственная точка входа: export * from "./defines"—
packages/core/src/defines/index.tsРеэкспорт functions, types, classes, core (файлы node.ts, callstack.ts, internals.ts наружу не реэкспортируются)—
packages/core/src/defines/core.tsМета-класс: реестр мета-инстансов и пять фабрик декораторовAomCore
packages/core/src/defines/node.tsУзел мета-инстанса: origin-дескрипторы, apply, inheritAomNode, NodeOriginDescriptor (не экспортируются наружу)
packages/core/src/defines/callstack.tsОчередь одной фазы и стек очередей по фазамAomCallstackQueue, AomCallstack, LazyCall (не экспортируются наружу)
packages/core/src/defines/internals.tsВнутренние типы, общие для файлов пакета: сигнатуры обработчиков, AomClass, origin-дескрипторAomHandler, AomCombinedHandler, AomMethodHandler, AomPropertyHandler, AomParameterHandler, AomClass, OriginDescriptor (не экспортируются наружу)
packages/core/src/defines/types.tsПубличные типы и перечисленияExecutionPhase, InheritPlacement, ClassConstructor, ClassType, Property, InstanceTarget, CombinedDecorator
packages/core/src/defines/classes.tsТипизированная обёртка над WeakMap<конструктор, экземпляр>ClassInstanceMap
packages/core/src/defines/functions.tsПроверка class-синтаксиса регуляркой по toStringisClass
packages/core/package.json, packages/core/tsconfig.build.json, packages/core/tsconfig.build.mjs.json, packages/core/tsconfig.build.types.jsonГенерируются scripts/prebuild.js из общей версии корневого package.json, package.src.json и scripts/jsons/package.dist.json; в .gitignore (правила packages/**/package.json и packages/**/tsconfig.build*.json)—

Реестр каждого мета-класса — WeakMap, лежащая на статическом свойстве самого класса под ключом Symbol.for(\AOM_MAP_${this.name}`); при первом обращении карта создаётся (packages/core/src/defines/core.ts, AomCore.instancesMap). Символ глобальный, но читается он с конкретного this`, поэтому мета-классы с разными именами всегда независимы.

Почему WeakMap, а не reflect-metadata: мета-инстанс — обычный изменяемый объект, декораторы дописывают его поля на месте, и записывать метаданные заново после каждого изменения не нужно. Ключи — сами классы и функции методов, поэтому реестр не удерживает их от сборщика мусора.

Ключ вычисляет instanceKey(target, property) (packages/core/src/defines/core.ts, AomCore.instanceKey). Конструктор берётся как сам target, если это функция, иначе target.constructor. Далее:

  • property не задан либо конструктор не является классом (isClass) → ключ = сам target;
  • property задан, дескриптор существует, у него есть value и он не enumerable (то есть это метод) → ключ = сама функция descriptor.value, поэтому Meta.instance(A, "run") и Meta.instance(A.run) дают один инстанс;
  • иначе (поле без дескриптора или enumerable-статика) → создаётся синтетический инстанс new this(this, constructor, property, originDescriptor), который кладётся в карту сам на себя (map.set(instanceKey, instanceKey)) и в root._.statics (если target === constructor) либо в root._.properties (если target — прототип).
Что декорируетсяВызовКлюч в реестреКуда попадает
Классinstance(A)сам класс A—
Статический методinstance(A, "run") = instance(A.run)функция A.runroot._.origins
Прототипный методinstance(A.prototype, "run")функция A.prototype.runroot._.origins
Поле инстансаinstance(A.prototype, "field")синтетический инстансroot._.properties + root._.origins
Статическое data-полеinstance(A, "config")синтетический инстансroot._.statics
Свойство не-class функцииinstance(fn, "x")сама fn— (карты не задействованы)

Последняя строка — следствие условия property && isClass(constructor) (packages/core/src/defines/core.ts, AomCore.instanceKey): для обычной функции или стрелки property игнорируется и ключом становится сам target, поэтому разные свойства одной функции получат один инстанс.

instance() (packages/core/src/defines/core.ts, AomCore.instance) ищет по ключу, при отсутствии создаёт инстанс и сохраняет. Отдельная ветка packages/core/src/defines/core.ts (AomCore.instance) чинит порядок обращений: если инстанс уже был создан по значению функции (без property), а теперь запрошен по паре «класс + свойство», его узел _ пересоздаётся с корректными target, property и root.

class A {
static handler() {}
}
const viaValue = Meta.instance(A.handler); // сначала по функции
const viaProperty = Meta.instance(A, "handler");
viaProperty === viaValue; // true
viaValue._.target; // A — узел пересоздан с корректными target и property
viaValue._.property; // "handler"

Узел (packages/core/src/defines/node.ts, AomNode) — служебная часть мета-инстанса, доступная как $._.

ПолеТипСодержание
baseмета-классКласс, чей реестр создал инстанс; через него узел вызывает instance()
targetClassConstructorКонструктор-владелец — всегда конструктор, и для статики, и для полей инстанса
propertyProperty?Ключ свойства; у узла класса не задан
selfмета-инстансСам инстанс, которому принадлежит узел
rootмета-инстансДля узла свойства — мета-инстанс класса, для узла класса — self
originInstanceInstanceTargetТо, что реально декорировалось: класс (статика) или прототип (свойство инстанса)
originsNodeOriginDescriptor[]Узлы декорированных свойств; наполняется только у корневого узла
properties / staticsMap<Property, T>Синтетические инстансы полей инстанса и статических data-полей
callstackAomCallstackОчереди фаз собственного узла
inheritedWeakSet<ClassType>Родители, чьи вызовы уже перенесены на этот узел; не даёт повторному inherit(parent) повторно выполнить его FIRST-очередь

origins наполняется только у корневого узла и только когда дескриптор отсутствует либо не enumerable (packages/core/src/defines/node.ts, AomNode.origins): туда попадают методы (статические и прототипные) и поля инстанса, но не enumerable-статические data-поля — те живут исключительно в statics. Разделение «статика или прототип» восстанавливается сравнением origin.target === $origin._.target (packages/core/src/defines/node.ts, AomNode.inherit); эту же формулу повторяют holder (packages/holder/src/decorators.ts, Apply) и mapped-types схемера (packages/schemer/src/defines/mapped-types/partial-type.helper.ts, PartialType).

apply() (packages/core/src/defines/node.ts, AomNode.apply) разбирает отложенные очереди в фиксированном порядке: сначала все узлы из properties, затем из statics, затем из origins, и только потом собственный callstack узла.

Зачем фазы. Legacy-декораторы вычисляются снизу вверх: сначала декораторы полей и методов, последним — декоратор класса. Декоратор метода не знает ни того, что поставят соседние декораторы, ни итогового класса — им может оказаться потомок, который унаследует метод. Поэтому работу, которой нужен собранный класс, декоратор не делает сразу, а кладёт в очередь LAST; запускает очередь декоратор-агрегатор класса (@Controller(), @ComponentSchema(), @Isolator()), который вычисляется последним и вызывает $._.apply(). То, что можно сделать сразу и нужно повторить на потомке, идёт в FIRST; сам агрегатор — в INSTANT, чтобы не повторяться при наследовании.

AomCallstack создаёт по одной очереди AomCallstackQueue на каждое значение ExecutionPhase — в stores (packages/core/src/defines/callstack.ts). Поведение очереди задаётся фазой в конструкторе AomCallstackQueue (packages/core/src/defines/callstack.ts): async = true только у DELAY, deferred = true у LAST и DELAY.

ФазаКогда выполняетсяГде сохраняетсяНаследуетсяВозврат доходит до декоратора
INSTANTв момент декорированиянигденет (см. ниже)нет
FIRSTв момент декорированияfiredда, с повторным выполнением на потомкенет
LASTпри _.apply() корневого узлаqueue → firedда, встаёт в очередь потомканет
DELAYв setTimeout после _.apply()queue → firedда, встаёт в очередь потомканет

add() (packages/core/src/defines/callstack.ts, AomCallstackQueue.add) реализует эту таблицу тремя ветками: deferred (LAST/DELAY) кладёт вызов в queue через push и ждёт exec(); FIRST кладёт вызов в fired через push и тут же выполняет, поэтому он и срабатывает сразу, и остаётся доступным для переноса на потомка; INSTANT только выполняет, нигде не сохраняя. Индекс параметра в размещении вызова не участвует: parameterIndex попадает только в аргументы обработчика (см. §«Фабрики декораторов»).

INSTANT-вызовы намеренно нигде не сохраняются: @Controller(), @ComponentSchema(), @Delay/@Apply holder’а вызывают $._.inherit()/$._.apply() и объявлены в фазе INSTANT именно затем, чтобы не сохраняться в очереди — иначе transferTo повторил бы вызов агрегатора на потомке и увёл бы в рекурсию.

exec() (packages/core/src/defines/callstack.ts, AomCallstackQueue.exec) вычерпывает очередь через while ((value = this.queue.shift())) и складывает выполненное в fired — повторный apply() те же LAST не повторяет. run() (packages/core/src/defines/callstack.ts, AomCallstackQueue.run) вызывает обработчик через Reflect.apply(handler, null, args); для DELAY — внутри setTimeout без аргумента задержки, поэтому возвращаемое значение обработчика теряется, а брошенное исключение никем не перехватывается (см. §«Ограничения»).

AomCallstack.apply() (packages/core/src/defines/callstack.ts) перебирает фазы в порядке объявления enum — INSTANT, FIRST, LAST, DELAY — и для каждой вызывает exec() на stores. Очереди INSTANT/FIRST к этому моменту пусты по построению.

Порядок фаз на примере:

const log: string[] = [];
const Mark = (label: string, phase: ExecutionPhase) =>
Meta.classDecorator(($) => log.push(label), phase);
// legacy-декораторы применяются снизу вверх: delay, last, first
@Mark("first", ExecutionPhase.FIRST)
@Mark("last", ExecutionPhase.LAST)
@Mark("delay", ExecutionPhase.DELAY)
class D {}
log; // ["first"] — при декорировании сработал только FIRST
Meta.instance(D)._.apply();
log; // ["first", "last"] — DELAY ещё нет, он через setTimeout
await new Promise((r) => setTimeout(r, 0));
log; // ["first", "last", "delay"]

Все пять фабрик устроены одинаково: захватывают this (мета-класс) в self, получают мета-инстанс через self.instance(target, property) и добавляют ленивый вызов в очередь заданной фазы. Различаются они целью и составом аргументов обработчика (packages/core/src/defines/core.ts, classDecorator, combinedDecorator, methodDecorator, propertyDecorator, parameterDecorator):

ФабрикаИнстанс берётся поАргументы обработчика
classDecoratorinstance(target)[$]
combinedDecoratorinstance(target, property)[$, descriptor] — descriptor определён только для метода
methodDecoratorinstance(target, property)[$, descriptor]
propertyDecoratorinstance(target, property)[$]
parameterDecoratorinstance(target, property) — для параметров конструктора property === undefined, значит узел класса[$, parameterIndex] — parameterIndex передаётся только обработчику, позицией в очереди не служит

Значение, возвращённое обработчиком, не доходит до TypeScript ни в одной фазе. AomCallstackQueue.add() действительно возвращает результат run() в ветках FIRST/INSTANT (packages/core/src/defines/callstack.ts), но ни одна фабрика не возвращает этот результат наружу декоратору — все они вызывают targetInstance._.callstack.add(...) (то есть AomCallstack.add(), packages/core/src/defines/callstack.ts, который сам вызывает store.add(value) тоже без return) как побочный эффект. Поэтому AomMethodHandler возвращает void (packages/core/src/defines/internals.ts) — тип соответствует фактическому поведению: methodDecorator не может подменить дескриптор метода возвращаемым значением ни в FIRST, ни в LAST.

AomNode.inherit(parent) (packages/core/src/defines/node.ts) выполняет два шага.

Перед этим — дедупликация: inherited — WeakSet<ClassType> с уже унаследованными родителями (packages/core/src/defines/node.ts, AomNode.inherited). Повторный inherit(того же parent) просто возвращает $parent без повторного переноса (packages/core/src/defines/node.ts, AomNode.inherit) — без этого каждый вызов заново выполнял бы FIRST-очередь родителя.

Шаг первый — перенос собственных вызовов класса-родителя: $parent._.callstack.transferTo(this.self). transferTo (packages/core/src/defines/callstack.ts, AomCallstack.transferTo, AomCallstackQueue.transferTo) для каждой фазы обходит [...fired, ...queue] и добавляет вызов в очередь потомка, подменив первый аргумент ($) на мета-инстанс потомка. Следствия: FIRST в момент inherit выполняется повторно — уже в контексте потомка; LAST/DELAY просто встают в очередь потомка. Размещение задаёт третий элемент ленивого вызова:

InheritPlacementОтложенные фазы (LAST/DELAY)FIRST
не задан / APPENDв конец очереди потомка, после его собственных вызововвыполняется на потомке после его собственных
PREPENDединым блоком в начало очереди потомка, взаимный порядок внутри блока сохраняетсякак APPEND — порядок уже задан моментом переноса
SKIPне переноситсяне переносится

Блок PREPEND собирается в отдельный массив и ставится в начало через prependBatch (packages/core/src/defines/callstack.ts) — поштучный unshift развернул бы порядок. Этим механизмом пользуется @Before роутера, чтобы родительские pre-middleware оказывались перед собственными (packages/router/src/decorators/middlewares/index.ts, Before).

Шаг второй — перенос декорированных свойств родителя по $parent._.origins (packages/core/src/defines/node.ts, AomNode.inherit). Перенос выполняется, только если потомок ещё не владеет свойством; проверок две и обе нужны: Object.hasOwn закрывает статики и прототипные методы, реестр (root._.statics / root._.properties) — поля класса, которых физически нет ни на конструкторе, ни на прототипе. При переносе метода transferDescritorWithDecorators (packages/core/src/defines/node.ts, AomNode.inherit) определяет у потомка собственное свойство с каррированной обёрткой:

packages/core/src/defines/node.ts (AomNode.inherit):

if (descriptor && descriptor.value) {
// здесь обязательно каррирование, так как иначе будет замена контекста исходного дескриптора изначального родительского класса
// this пробрасывается вызывающей стороне (не фиксируется на null): для статики это
// конкретный класс, для прототипного метода — инстанс, на котором его вызвали
const curriedDescriptor = function (this: any, ...args: any[]) {
return Reflect.apply(descriptor.value, this, args);
};
// определим
Reflect.defineProperty(target, property, {
...descriptor,
value: curriedDescriptor,
});
}

Копия — другая функция, значит и другой ключ в реестре: у потомка появляется собственный мета-инстанс метода, на который переносятся декораторы родителя. Каррированная копия — обычная function, и this внутри — тот, с которым вызвали копию (класс-потомок для статики, инстанс — для прототипного метода). Идентичность методов родителя и потомка при этом разная (Child.run !== Parent.run), поэтому сравнивать нужно по мета-инстансу.

Порядок вызовов inherit по цепочке задаёт вызывающая сторона. @Controller() идёт от ближайшего родителя вверх (packages/router/src/decorators/controllers/index.ts, Controller), поэтому при PREPEND блок прародителя оказывается перед блоком родителя.

Декораторы параметров конструктора — особый случай transferTo: если у потомка свой конструктор со своими декорированными параметрами, родительские декораторы параметров конструктора не переносятся — иначе они встали бы в очередь после собственных и перезаписали бы их результат (AomCallstackQueue.hasParameters() и skipParameters в AomCallstack.transferTo, packages/core/src/defines/callstack.ts; признак — четвёртый элемент ленивого вызова, parameter?: boolean, LazyCall). Потомок без своего конструктора получает декораторы параметров конструктора родителя; предел распознавания — в §«Ограничения».

Вызовы параметр-декораторов лежат в очереди подряд, без привязки к индексу параметра, поэтому перенос метода с FIRST-декоратором не на первом параметре проходит без ошибки:

const seen: string[] = [];
const Param = () =>
Meta.parameterDecorator(($, index) => seen.push(`${$._.target.name}:${index}`));
class Parent {
static run(a: string, @Param() b: string) {}
}
class Child extends Parent {}
Meta.instance(Child)._.inherit(Parent); // не бросает
seen; // ["Parent:1", "Child:1"]

resolveFor($, context) решает задачу поздней привязки. Ссылка вида @Before(Self.Method) вычисляется в момент декорирования, когда @Controller() потомка ещё не отработал, и потому фиксирует мета-инстанс статики родителя. После inherit() у потомка появляется собственная каррированная копия метода со своим мета-инстансом, и ссылку нужно доразрешить.

packages/core/src/defines/core.ts (AomCore.resolveFor):

// позднее доразрешение ссылки на статику по контекстному классу: если инстанс `$`
// привязан к родителю контекста, вернуть инстанс каррированной копии потомка
static resolveFor<T extends AomCore>(this: AomClass<T>, $: T, context: ClassConstructor): T {
const { target, property } = $._;
// Object.hasOwn обязателен: собственная копия появляется у потомка только при inherit();
// без неё instance() создал бы пустой инстанс без дескриптора вместо корректного
if (property && context.prototype instanceof target && Object.hasOwn(context, property)) {
return this.instance(context, property);
}
return $;
}

Три условия проверяются вместе: $ описывает свойство, context — потомок владельца $, и у context уже есть собственная копия свойства. Не выполнено любое — возвращается исходный $, поэтому вызов безопасен на произвольной ссылке.

Единственный потребитель — router: AomCursor перепривязывает $ при создании курсора (packages/router/src/common/cursor.ts), а @Before/@After/@UseMarker/@Next доразрешают ссылки на middleware в контексте класса, объявившего декоратор (packages/router/src/decorators/middlewares/index.ts, Before, After, UseMarker, Next).

  • Мета-класс — наследник AomCore с собственными полями состояния; фабрики вызываются на самом мета-классе (Meta.methodDecorator(($) => …)). Деструктурировать фабрику нельзя: this внутри — это мета-класс, из него берутся реестр и конструктор инстанса.
  • Ссылки на владельца внутри обработчика — только через узел: $._.target, $._.property, $._.root. Прикладные данные класса складываются в $._.root, данные свойства — в сам $.
  • Декораторы-агрегаторы, вызывающие $._.inherit() и $._.apply(), объявляются в фазе INSTANT — иначе перенос на потомка повторит агрегатор и вызовет рекурсию. Так сделаны @Controller() (packages/router/src/decorators/controllers/index.ts, Controller), @ComponentSchema() (packages/schemer/src/defines/decorators.ts, ComponentSchema), @Delay (packages/holder/src/decorators.ts, Delay); @Apply объявлен в той же фазе и делает то же самое опосредованно — через Reflect.decorate([Delay(...)]) (packages/holder/src/decorators.ts, Apply). Поэтому INSTANT-вызовы намеренно нигде не сохраняются и не наследуются (см. §«Ограничения»).
  • LAST-вызовы свойств разбираются только через apply() корневого узла класса: без агрегатора на классе отложенные декораторы методов не выполнятся никогда.
  • Порядок применения legacy-декораторов задаёт TypeScript: методы класса декорируются раньше самого класса, а несколько декораторов на одной цели применяются снизу вверх.
  • Реестры мета-классов с разными именами независимы (Symbol.for читается с конкретного this). Слияние возможно только у наследника мета-класса с тем же name: символ совпадает, собственного свойства у наследника ещё нет, и он читает карту родителя через цепочку статического наследования.
  • Относительные импорты в src пишутся без расширений — tsc их не подставляет, расширения дописывает scripts/post-build.js в dist; спецификаторы, уже написанные с расширением (js, json, mjs, cjs), скрипт пропускает как есть, падает он только на неразрешимых путях — копит их в errors и завершается с кодом 1 (scripts/post-build.js (SPEC, errors)).
  • strictPropertyInitialization выключен намеренно: поля узлов и мета-классов заполняют декораторы, а не конструктор (tsconfig.base.json).

Приложение на ядре может объявить собственный мета-класс, например реестр «метод эндпоинта → требуемая роль»:

class RoleRelay extends AomCore {
required: { target: ClassConstructor; property: Property; role: string }[] = [];
}
const RequireRole = (role: string) =>
RoleRelay.methodDecorator(($) => {
const { target, property } = $._;
$._.root.required.push({ target, property, role });
Reflect.decorate([Before(CheckRole) as MethodDecorator], target, property!);
});

Здесь видны сразу три приёма из §«Ключевые механизмы»: владелец и имя метода берутся из узла ($._.target, $._.property), накопитель кладётся в мета-инстанс класса ($._.root), а не свойства, и обработчик сам навешивает декораторы роутера через Reflect.decorate — то есть мета-класс выступает генератором маршрутных декораторов. В рантайме middleware CheckRole берёт мета-инстанс класса-владельца курсора через RoleRelay.instance(origin._.target) и находит запись по origin._.property.

Косвенно ядро задействовано в каждом классе приложения на ядре: @Controller(), @ComponentSchema(), @Isolator() и все параметр-декораторы маршрутов — это фабрики AomCore поверх мета-классов router, schemer и transfer.

  • INSTANT не наследуется. Вызовы фазы INSTANT нигде не сохраняются (AomCallstackQueue.add, packages/core/src/defines/callstack.ts), поэтому перенос на потомка их не повторяет — так агрегаторы не уходят в рекурсию (см. §«Инварианты и подводные камни»). По той же причине holder сам переносит $.queue родительского AomHolder-инстанса перед inherit(), иначе класс-декораторы из @Delay родителя не доезжали бы до потомка при @Apply (@aom-js/holder). Как обойти: для наследуемого поведения выбирать FIRST.
  • Методы потомка — другие функции. После inherit каррированная копия метода — другая функция с собственным мета-инстансом, поэтому Child.run !== Parent.run. Как обойти: сравнивать методы родителя и потомка по мета-инстансу (AomApi.instance(...)), а не по ссылке на функцию.
  • checkExists со свойством может создать инстанс. Для свойства без дескриптора checkExists идёт по той же ветке instanceKey, что и instance(), и регистрирует синтетический инстанс (AomCore.instanceKey, AomCore.checkExists, packages/core/src/defines/core.ts). Как обойти: передавать в checkExists только класс.
  • Исключение в DELAY роняет процесс. Обработчик DELAY выполняется в setTimeout без try/catch (AomCallstackQueue.run), и его ошибка становится необработанным исключением (uncaughtException/unhandledRejection). Как обойти: не класть в обработчик DELAY код, который может бросить исключение.
  • Возврат обработчика теряется. Значение, возвращённое обработчиком, не доходит до TypeScript ни в одной фазе, поэтому обработчик метода типизирован как void и не может подменить дескриптор (см. §«Фабрики декораторов»).
  • Свой конструктор потомка без декорированных параметров не распознаётся. Родительские декораторы параметров конструктора перенесутся на такого потомка, хотя конструктор у него свой (AomCallstack.transferTo); точное распознавание требует чтения design:paramtypes. Как обойти: если у потомка свой конструктор, декорировать его параметры явно.

Сборка пакета — bun run build:core в корне ядра (эквивалент bun --filter @aom-js/core build, package.json (typescript)): три прохода tsc по сгенерированным конфигам (tsconfig.build.json → dist/cjs, tsconfig.build.mjs.json → dist/mjs, tsconfig.build.types.json → dist/types), затем node ../../scripts/post-build.js, который дописывает расширения относительным импортам в собранном коде и кладёт dist/cjs/package.json с {"type":"commonjs"}. Сам package.json и все tsconfig.build*.json генерирует scripts/prebuild.js и они игнорируются git — править нужно package.src.json и шаблоны в scripts/jsons.

Потребитель резолвит пакет через exports в dist/mjs (dist/cjs — для require). Отсюда инвариант: правки src не видны потребителю без bun run build. Полная схема сборки монорепо и dist-потребления — Архитектура ядра.

Из корня ядра: bunx vitest run packages/core (все пакеты — bun run test).