Skip to content

@This, @OnInstance и жизненный цикл объекта класса

This content is not available in your language yet.

Рецепт о том, когда объект класса становится виден другому коду и что к этому моменту про него гарантировано.


У объекта класса внутри запроса две стадии:

  1. создан — конструктор отработал, хуки @OnInstance ещё идут. Объект виден только коду самого класса;
  2. проверен — все хуки прошли успешно. Только тогда объект попадает в общую карту запроса и становится виден @This, @State и курсорам маршрута из чужих классов.

Если хук отказал, второй стадии не наступает вообще: в общую карту не попадает ничего, а все, кто обратился к классу в этом запросе, получают ту же ошибку.

Отсюда следует всё остальное.


Рецепт 1: зависимый класс берёт объект в конструкторе

Заголовок раздела «Рецепт 1: зависимый класс берёт объект в конструкторе»

Обычный и правильный приём.

@Controller()
class Auth {
token = "SECRET";
verified = false;
@OnInstance()
static async Verify(@This() self: Auth, @Headers("authorization") token: string) {
// поход в базу: здесь исполнение прерывается и отдаёт управление другим веткам
await db.check(token);
if (token !== "ok") throw new Error("401");
self.verified = true;
}
}
@Controller()
class Dependent {
// конструктор дождётся окончания хуков Auth и получит заполненный ими объект
constructor(@This(Auth) public auth: Auth) {}
}
@Controller()
class App {
@Get("/dependent")
static async dependent(@This(Dependent) dep: Dependent) {
return `${dep.auth.token}/${dep.auth.verified}`; // "SECRET/true"
}
}

Что гарантировано. К моменту входа в конструктор Dependent хук Verify уже отработал: поле verified заполнено, token проверен.

Чем платим. Ожиданием: конструктор Dependent не начнётся, пока идут хуки Auth. Это ровно то время, которое хуки и так занимают, — параллельность нигде не теряется, просто зависимый код не забегает вперёд.


Рецепт 2: отказ проверки останавливает всё разом

Заголовок раздела «Рецепт 2: отказ проверки останавливает всё разом»
@Controller()
class Dependent {
constructor(@This(Auth) public auth: Auth) {
sideEffect(); // НЕ выполнится, если проверка Auth отказала
}
}

При неверном токене запрос отвечает 401, и ни конструктор Dependent, ни эндпоинт не выполняются вовсе. Проверять что-то ещё раз в зависимом классе не нужно: сам факт того, что он построился, означает, что все проверки его зависимостей прошли.


Антирецепт: хук отдаёт свой объект чужому классу через next()

Заголовок раздела «Антирецепт: хук отдаёт свой объект чужому классу через next()»

Так писать нельзя: это обход проверки доступа.

// ❌ нельзя: чужой класс просит объект, чьи проверки ещё идут
@Controller()
class Auth {
user?: User;
@OnInstance()
static async Verify(@This() self: Auth, @Headers("authorization") token: string) {
self.user = await db.findUserByToken(token); // окно: управление ушло в базу
if (!self.user) throw new Error("401");
}
@OnInstance()
static async Audit(@Run() next: NextFn) {
await next(Metrics.write); // соседний хук идёт ОДНОВРЕМЕННО с Verify
}
}
@Controller()
class Metrics {
@Middleware()
static async write(@This(Auth) auth: Auth) {
// Verify ещё висит на базе, объект не проверен
await analytics.send({ user: auth.user });
}
}

Чем это плохо: Metrics.write — чужой класс. Получи он объект, он успел бы отработать до того, как Verify скажет «401»: отправить событие, записать метрику, сходить в сторонний сервис. Запрос в итоге отказал бы, но постороннее действие уже случилось бы. Для проверки доступа это и есть обход — решение «нельзя» принималось бы позже, чем случилось действие. Плюс auth.user в этот момент может быть ещё undefined, и дальше по коду это всплыло бы не ошибкой, а неверными данными.

Поэтому такой код даёт CIRCULAR_INSTANCE_ERROR — чужой класс не может получить объект, чьи проверки ещё идут.

// так: объект передаётся помощнику обычным аргументом
@OnInstance()
static async Audit(@This() self: Auth) {
await Metrics.write(self);
}

Разница принципиальная: во втором варианте Metrics не запрашивает объект у фреймворка, а получает его от того, кто имеет право им распоряжаться. Ответственность за то, что объект ещё не проверен, остаётся на Auth.


Что можно внутри собственных хуков класса

Заголовок раздела «Что можно внутри собственных хуков класса»

Собственный код класса работает во время его же хуков без ожидания — иначе класс запер бы сам себя.

@Controller()
class Auth {
token = "PRIVATE";
@OnInstance()
static async load(@Run() next: NextFn) {
await next(Auth.Privileged); // своя middleware — можно, вложенность любая
}
@OnInstance()
static async self(@This() self: Auth) {
// объект ещё НЕ проверен: другие хуки могут идти прямо сейчас
return prepare(self);
}
@Middleware()
static async Privileged(@This() self: Auth) {
return self.token; // тоже свой код — объект отдаётся сразу
}
}

Плата за это — ваша ответственность. Собственный хук видит ещё не проверенный объект. Значит, он не должен выпускать его наружу: класть в ctx, сохранять в модульную переменную, передавать в фоновую задачу. Кодом фреймворка это не чинится — снаружи такой объект уже не отличить от проверенного.


Особый случай: класс, созданный изнутри хука

Заголовок раздела «Особый случай: класс, созданный изнутри хука»

Если хук сам создаёт другой класс, тот в своём конструкторе увидит создателя ещё не проверенным:

@Controller()
class Auth {
verified = false;
@OnInstance()
static async Verify(@This() self: Auth, @This(FwdRef(() => Sidecar)) side: Sidecar) {
self.verified = true;
}
}
@Controller()
class Sidecar {
constructor(@This(Auth) auth: Auth) {
auth.verified; // false — Verify ещё не дошёл до присваивания
}
}

Это не дыра, а неизбежное следствие: Sidecar создаётся внутри собственной ветки создания Auth, и заставить его ждать — значит заставить Auth ждать самого себя.

Практическое правило: в классе, который создаётся изнутри чужого хука, нельзя рассчитывать на результат этого хука. Если нужна проверенная зависимость — создавайте такой класс не из хука, а из маршрута (middleware или аргумент эндпоинта).


Порядок создания решает: одна и та же пара классов

Заголовок раздела «Порядок создания решает: одна и та же пара классов»

Неочевидное и важное. Та же пара Auth ↔ Sidecar может работать, а может дать ошибку цикла — в зависимости от того, кто из них начал создаваться первым.

// работает: Sidecar создаёт хук Auth, значит он внутри ветки Auth
@Get("/sidecar")
static async sidecar(@This(Auth) auth: Auth) { ... }
// ❌ CIRCULAR_INSTANCE_ERROR: Auth -> Sidecar -> Auth
// эндпоинт просит оба класса сразу, они создаются параллельно, и хук Auth
// упирается в создание Sidecar, которое уже ждёт Auth
@Get("/both")
static async both(@This(Sidecar) side: Sidecar, @This(Auth) auth: Auth) { ... }

Правило: если два класса ссылаются друг на друга, не просите их у фреймворка одновременно — ни соседними аргументами одного метода, ни соседними middleware. А лучше уберите взаимную ссылку: это почти всегда признак того, что общую часть пора вынести в третий класс.


CIRCULAR_INSTANCE_ERROR всегда несёт цепочку классов — читать её надо как «кто кого потянул»:

CIRCULAR_INSTANCE_ERROR: Left -> Right -> Left

Значит: создавали Left, он потребовал Right, а тот снова потребовал Left. Чинить — разрывать эту связь, а не ловить ошибку.

Та же ошибка с цепочкой классов приходит при любом цикле: взаимные @This в конструкторах, цикл «конструктор ↔ хук», взаимные хуки двух классов, @This на себя внутри своего конструктора. Без неё такой цикл дал бы RangeError: Maximum call stack size exceeded, запрос без ответа или молча отданный непроверенный объект.


Хуки @OnInstance одного класса выполняются параллельно, и уже запущенный хук не отменяется, когда соседний отказал. То есть если один хук пишет в базу, а второй в это время отвечает «401», запись всё равно произойдёт.

Хуки идут параллельно намеренно: при последовательном выполнении время создания класса было бы суммой хуков вместо максимума, а хуки потомка выполняются раньше хуков родителя, и унаследованная проверка оказалась бы последней. Правило простое: побочные эффекты в хуках должны быть отменяемыми или идти после проверок в том же хуке.


  • Справочник по декораторам: @aom-js/router, разделы про @OnInstance, @This, @State, @Run
  • Порядок исполнения маршрута целиком: Архитектура ядра
  • Тесты, исполняющие этот рецепт: packages/router/test/router-instance-safety.test.ts