Skip to content

Commit ebfbf52

Browse files
killaguclaude
andauthored
feat(tegg): isolate per-app state for concurrent multi-app via TeggScope (#5986)
## Status update (latest) - Merged current `next` (resolved the aop + MCPControllerRegister conflicts) and CI is **green** across the full matrix (macOS/Ubuntu/Windows × Node 22/24), both E2E suites, typecheck/fmt/lint, and project coverage. - **Single-app correctness fix:** per-app state installed in the app's `TeggScope` bag was unreachable from code running *outside* an explicit `TeggScope.run` (a singleton/ContextProto method or detached logger called directly). The no-scope fallback now resolves to the *sole live app's bag* when exactly one app is alive, restoring pre-scoping single-app behavior while keeping multi-app isolation + the escape fuse. This fixed a real `@eggjs/orm-plugin` regression (leoric's SQL logger threw `getContextCallback not set`, swallowed → no SQL logged). - **Simplification pass** (net −56 lines): `TeggScope.runMaybe`, `EggPrototypeFactory.getPrototypeByClazzOrGlobal` (centralizes the rule-4 fallback), `defineScopedLifecycleUtil` tuple helper, a uniform `runInScope` in the standalone Runner, dropped dead code (`_setDefaultBag`, controller `willReady` MCP block) — all behavior-preserving. - **Adversarial review fixes:** lazily resolve `EggPrototypeFactory.instance` in langchain's `GraphLoadUnitHook.preCreate` (it had captured the factory in the boot constructor → cross-app pollution under multi-app); guarantee `TeggScope.unregisterScope` on every teardown/error path in the standalone Runner + `main()` + the tegg plugin `beforeClose` (scope-registry leaks would have flipped `isMultiApp` on for later apps). - Added `TeggScope` unit coverage for the scope/fallback/escape behavior. --- ## Motivation Today tegg keeps a lot of runtime state in **process-global statics** (factory registries keyed by deterministic name, `ContextHandler` callbacks, singleton managers, lifecycle utils…). This makes it a "one active app at a time" design — two tegg apps in one process clobber each other. This PR makes per-app state **isolated** so multiple apps can boot and serve requests **concurrently** in one process without cross-talk. ## Approach: `TeggScope` + per-package slots A new low-level, type-free `TeggScope` (`AsyncLocalStorage<Map<symbol, unknown>>`) in `@eggjs/tegg-types`: - Each package defines its own slot (symbol) + concrete fallback and **only imports `TeggScope`** — never another package. Dependency direction stays downward (metadata never imports runtime). - Static facades keep the **same call sites** (`EggPrototypeFactory.instance`, `GlobalGraph.instance`, `ContextHandler`, dal managers, lifecycle utils, …) but resolve from the current app's bag. - The aggregator lives in `plugin/tegg` (`app._teggScopeBag`, created in `configWillLoad`); boot hooks, request middleware, `mockModuleContextScope`, `getEggObject`, the `app.module`/`ctx.module` proxies, the standalone `Runner`, and escape points wrap work in `TeggScope.run(bag, …)`. - **Strict-mode escape fuse**: under true multi-app (`> 1` live app) any access that escapes to the process-default bag throws in dev / warns in prod. Single-app keeps a silent lazy default — **no behavior change for existing single-app code/tests**. ## Scope (request-reachability) - **core/metadata**: `EggPrototypeFactory` (+ per-app `class→proto` map), `GlobalGraph`, `LoadUnitFactory` (two-tier creator map), `LoadUnitMultiInstanceProtoHook`, prototype/load-unit lifecycle utils. - **core/runtime**: `LoadUnitInstanceFactory`, `EggObjectFactory`, `ContextHandler`, object/context/load-unit-instance lifecycle utils. - **core/common-util**: `ModuleConfigUtil.configNames` (fixes the standalone config-names race). - **plugins**: tegg, controller (+ HTTP/MCP register), aop, dal (managers + app-extend getter), eventbus (per-app `SingletonEventBus`, `doEmit` re-establishes the owning app's scope for detached emits), orm, schedule, langchain, mcp-proxy. - **standalone**: per-`Runner` scope wrapping load/init/run/destroy. ## Two issues found beyond the original design 1. **Class→proto shared state** — `PrototypeUtil.getClazzProto` stores one proto per class globally, so concurrent boot races. Added per-app `EggPrototypeFactory.getPrototypeByClazz`, preferred in `getOrCreateEggObjectFromClazz` and `ctx.getEggObject`. 2. **Per-app lifecycle utils ⇒ every plugin registering lifecycle hooks must wrap registration in the app scope** (otherwise hooks land in the default bag and never fire during boot). All affected plugins updated. ## Testing - New `plugin/tegg/test/MultiApp.test.ts`: two concurrent apps sharing **one** module, asserting isolation of singletons, a per-app data store, EventBus emit/handler dispatch, and background tasks, plus sequential no-leak — **all under strict mode** (the escape fuse stays silent ⇒ zero escapes). - Full tegg suite: **645 passed**. The only two failures (`@eggjs/orm-plugin` `Unknown database 'test'`, `@eggjs/agent-runtime` "stream … during reconnect") **reproduce on `next` unchanged** — pre-existing env/flaky, not regressions. Standalone's previously-flaky `should work with env` now **passes** (per-Runner config names). - typecheck + oxlint + oxfmt clean on all changed files. ## Notes for reviewers - This is deliberately one branch for end-to-end review; I plan to split it into focused PRs (TeggScope keystone → core facades → plugin/tegg aggregator → per-plugin scoping → standalone → fixtures/tests). - No public API call sites change; the only new surface is `@eggjs/tegg-types` exporting `TeggScope`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added stronger multi-app isolation so each app keeps separate runtime state, caches, lifecycle utilities, and scoped singleton behavior. * Improved standalone boot/runtime to run initialization and teardown within an isolated execution scope. * **Bug Fixes** * Fixed cross-app leakage for configs, graphs, controllers, event handling, and per-request context callbacks. * Improved prototype and lifecycle resolution to stay correctly app-scoped across async boundaries. * **Documentation** * Added contributor guidance for maintaining multi-app safe scope isolation. * **Tests** * Added multi-app isolation and concurrent startup coverage. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 42f7a14 commit ebfbf52

80 files changed

Lines changed: 1955 additions & 416 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ Then re-run tests.
4646
- keep file names lowercase with hyphens
4747
- keep public API changes deliberate and documented
4848
- use `oxfmt` and `oxlint --type-aware` conventions already present in the repo
49+
- **tegg multi-app isolation**: do NOT introduce new process-global mutable
50+
runtime state in `tegg/`; per-app state must be backed by a `TeggScope` slot.
51+
Hooks registered through the bag-pinned `app.*LifecycleUtil` getters need no
52+
extra wrap; detached/escape-point access (timers, emitter listeners, proxy
53+
handlers, module-level lifecycle-util statics) must run inside
54+
`TeggScope.run(app._teggScopeBag, ...)`. See the "Multi-App Isolation
55+
(TeggScope)" section in `tegg/CLAUDE.md` for the full rules.
4956

5057
## TypeScript Global Types
5158

tegg/CLAUDE.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,89 @@ const impl = await eggObjectFactory.getEggObject(
285285
);
286286
```
287287

288+
## Multi-App Isolation (TeggScope) — MUST follow
289+
290+
Tegg supports multiple apps booting and serving requests **concurrently in one
291+
process** without cross-talk. This is built on `TeggScope`
292+
(`@eggjs/tegg-types`), a type-free `AsyncLocalStorage<Map<symbol, unknown>>`.
293+
Each app owns a per-app "bag" (`app._teggScopeBag`); per-app state lives in
294+
slots inside that bag, and the active bag is established with
295+
`TeggScope.run(app._teggScopeBag, ...)`. Per-app singletons resolve via the same
296+
old static call sites (e.g. `EggPrototypeFactory.instance`) — they now read the
297+
current scope instead of a process global.
298+
299+
When you touch tegg core/plugins, follow these rules:
300+
301+
1. **Never add new process-global mutable runtime state.** A `static` field /
302+
`Map` / singleton that holds per-app data WILL leak across concurrent apps.
303+
If you need such state, back it with a `TeggScope` slot:
304+
305+
```ts
306+
import { TeggScope } from '@eggjs/tegg-types';
307+
const X_SLOT = Symbol('tegg:<pkg>:<name>'); // module-private, never exported
308+
export class X {
309+
static get instance(): X {
310+
return TeggScope.resolve(X_SLOT, () => new X(), 'X.instance');
311+
}
312+
}
313+
```
314+
315+
Import `TeggScope` **only** from `@eggjs/tegg-types`; never import another
316+
package's slot. A package that imports `TeggScope` must declare
317+
`@eggjs/tegg-types` as a direct dependency.
318+
319+
2. **Shared, app-agnostic registries stay global.** Class/type-keyed maps
320+
populated at import time with app-agnostic values (e.g.
321+
`EggPrototypeCreatorFactory` creator map, `registerEggObjectCreateMethod`,
322+
`registerLoadUnitInstanceClass`) must NOT be scoped. Only state that holds
323+
per-app instances/data is scoped. (`LoadUnitFactory`'s creator map is
324+
two-tier: a global base for import-time creators + a per-app overlay for
325+
boot-time, app-capturing creators.)
326+
327+
3. **Lifecycle-hook registration via `app.*LifecycleUtil` is bag-pinned.**
328+
Calling `app.{loadUnit,eggPrototype,eggObject,eggContext,loadUnitInstance}LifecycleUtil.registerLifecycle(hook)`
329+
(and the matching `deleteLifecycle` in `beforeClose`) does **not** need a
330+
`TeggScope.run` wrap — these app getters are pinned to this app's bag (via
331+
`xxxLifecycleUtilFromBag`), so they resolve the correct per-app util even with
332+
no active scope. Wrapping is still fine when the same block does other
333+
scope-dependent work (as the tegg plugin's own boot does). Do **not** register
334+
lifecycle hooks in the boot **constructor**`app._teggScopeBag` does not
335+
exist yet; do it in `configWillLoad`/`configDidLoad`/`didLoad`. (Accessing a
336+
lifecycle util through a module-level static instead of `app.*LifecycleUtil`
337+
still needs an active scope.)
338+
339+
4. **Resolve egg objects per-app.** To get a proto from a class, prefer
340+
`EggPrototypeFactory.instance.getPrototypeByClazz(clazz)` (per-app) before
341+
falling back to `PrototypeUtil.getClazzProto(clazz)` (a process-global slot
342+
on the class, overwritten by concurrent boot). `ctx.getEggObject` /
343+
`app.getEggObject` already do this and wrap in the app scope.
344+
345+
5. **Escape points** — code that runs **detached** from the request must
346+
re-establish the scope. Capture `const bag = TeggScope.current()` at
347+
registration/scheduling and re-enter `TeggScope.run(bag, cb)` inside the
348+
callback for: emitter listeners triggered later (`res.on('close')`,
349+
`signal.addEventListener('abort')`), fire-and-forget `EventBus.emit` from a
350+
detached context, and timers created outside a scope. Timers/promises created
351+
**inside** an active scope inherit it automatically — no wrap needed.
352+
353+
6. **Strict-mode fuse.** Under true multi-app (`> 1` live app) any access that
354+
escapes to the process-default bag throws in dev / warns in prod. If you see
355+
`[tegg] TeggScope escaped to the process-default bag`, you have an unwrapped
356+
access — wrap the relevant boot/request/escape path in `TeggScope.run`.
357+
358+
7. **Single app is unchanged.** With one app the default bag is used silently
359+
and the fuse never fires, so existing single-app behavior and tests are
360+
unaffected. Add multi-app regression coverage (two concurrent apps sharing a
361+
module) when you change loader/runtime/lifecycle/eventbus behavior — see
362+
`tegg/plugin/tegg/test/MultiApp.test.ts`.
363+
364+
**Performance:** `TeggScope.resolve` adds ~8 ns/access and `TeggScope.run`
365+
~5 ns/call over a plain static read (Node 22); egg already runs on
366+
AsyncLocalStorage, so there is no new process-wide async penalty. The cost is
367+
negligible relative to real request work. The per-app lifecycle-util facade is an
368+
explicit delegating object (not a `Proxy`) — each method is a direct slot-resolve
369+
plus a method call, with no per-access trap or bound-function allocation.
370+
288371
## Common Patterns
289372

290373
### Creating a New Core Package

tegg/core/aop-runtime/src/LoadUnitAopHook.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { AspectInfoUtil, AspectMetaBuilder, CrosscutAdviceFactory } from '@eggjs/aop-decorator';
2-
import { PrototypeUtil } from '@eggjs/core-decorator';
3-
import { TeggError } from '@eggjs/metadata';
2+
import { EggPrototypeFactory, TeggError } from '@eggjs/metadata';
43
import type {
54
EggPrototype,
65
EggPrototypeWithClazz,
@@ -29,7 +28,7 @@ export class LoadUnitAopHook implements LifecycleHook<LoadUnitLifecycleContext,
2928
AspectInfoUtil.setAspectList(aspectList, clazz);
3029
for (const aspect of aspectList) {
3130
for (const advice of aspect.adviceList) {
32-
const adviceProto = PrototypeUtil.getClazzProto(advice.clazz);
31+
const adviceProto = EggPrototypeFactory.instance.getPrototypeByClazzOrGlobal(advice.clazz);
3332
if (!adviceProto) {
3433
throw TeggError.create(`Aop Advice(${advice.clazz.name}) not found in loadUnits`, 'advice_not_found');
3534
}

tegg/core/common-util/src/ModuleConfig.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
NpmModuleReferenceConfig,
1111
ReadModuleReferenceOptions,
1212
} from '@eggjs/tegg-types';
13+
import { TeggScope } from '@eggjs/tegg-types';
1314
import { importResolve } from '@eggjs/utils';
1415
import { extend } from 'extend2';
1516
import globby from 'globby';
@@ -33,8 +34,20 @@ const DEFAULT_READ_MODULE_REF_OPTS = {
3334
deep: 10,
3435
};
3536

37+
const CONFIG_NAMES_SLOT = Symbol('tegg:common-util:moduleConfigNames');
38+
3639
export class ModuleConfigUtil {
37-
static configNames: string[] | undefined;
40+
// Per-app/per-Runner: each standalone Runner (and app) has distinct config
41+
// names (env-based); a process-global static races across them (the standalone
42+
// "should work with env" ordering bug). Backed by TeggScope; with no active
43+
// scope it uses the single process-default bag (single-app / config-plugin boot).
44+
static get configNames(): string[] | undefined {
45+
return TeggScope.getOr(CONFIG_NAMES_SLOT, () => undefined, 'ModuleConfigUtil.configNames');
46+
}
47+
48+
static set configNames(configNames: string[] | undefined) {
49+
TeggScope.set(CONFIG_NAMES_SLOT, configNames);
50+
}
3851

3952
public static setConfigNames(configNames: string[] | undefined): void {
4053
ModuleConfigUtil.configNames = configNames;

tegg/core/dynamic-inject-runtime/src/EggObjectFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { PrototypeUtil, SingletonProto } from '@eggjs/core-decorator';
1+
import { SingletonProto } from '@eggjs/core-decorator';
22
import { QualifierImplUtil } from '@eggjs/dynamic-inject';
3+
import { EggPrototypeFactory } from '@eggjs/metadata';
34
import type { EggContainerFactory } from '@eggjs/tegg-runtime';
45
import { AccessLevel } from '@eggjs/tegg-types';
56
import type { QualifierValue, EggAbstractClazz, EggObjectFactory as IEggObjectFactory } from '@eggjs/tegg-types';
@@ -19,7 +20,7 @@ export class EggObjectFactory implements IEggObjectFactory {
1920
if (!implClazz) {
2021
throw new Error(`has no impl for ${abstractClazz.name} with qualifier ${qualifierValue}`);
2122
}
22-
const protoObj: any = PrototypeUtil.getClazzProto(implClazz);
23+
const protoObj: any = EggPrototypeFactory.instance.getPrototypeByClazzOrGlobal(implClazz);
2324
if (!protoObj) {
2425
throw new Error(`can not get proto for clazz ${implClazz.name}`);
2526
}

tegg/core/eventbus-runtime/src/SingletonEventBus.ts

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { Inject, SingletonProto } from '@eggjs/core-decorator';
44
import { type EventBus, type Events, type EventWaiter, type EventName, CORK_ID } from '@eggjs/eventbus-decorator';
55
import type { Arguments } from '@eggjs/eventbus-decorator';
66
import { ContextHandler } from '@eggjs/tegg-runtime';
7-
import { AccessLevel } from '@eggjs/tegg-types';
8-
import type { EggRuntimeContext } from '@eggjs/tegg-types';
7+
import { AccessLevel, TeggScope } from '@eggjs/tegg-types';
8+
import type { EggRuntimeContext, TeggScopeBag } from '@eggjs/tegg-types';
99
// @ts-expect-error await-event is not typed
1010
import awaitEvent from 'await-event';
1111
// @ts-expect-error await-first is not typed
@@ -49,6 +49,12 @@ export class SingletonEventBus implements EventBus, EventWaiter {
4949

5050
private readonly corkedEvents = new Map<string /* corkId */, CorkEvents>();
5151

52+
// The per-app TeggScope bag captured when this (per-app) singleton was created.
53+
// emit() is fire-and-forget, so handlers may run on a later tick or be triggered
54+
// from a detached context; re-establishing this bag in doEmit keeps event
55+
// handlers bound to the owning app's factories regardless of the ambient scope.
56+
private readonly teggScopeBag: TeggScopeBag | undefined = TeggScope.current();
57+
5258
/**
5359
* only use for ensure event will happen
5460
*/
@@ -144,36 +150,39 @@ export class SingletonEventBus implements EventBus, EventWaiter {
144150
}
145151

146152
private async doEmit(ctx: EggRuntimeContext, event: EventName, args: Array<any>) {
147-
await ContextHandler.run(ctx, async () => {
148-
const lifecycle = {};
149-
if (ctx.init) {
150-
await ctx.init(lifecycle);
151-
}
152-
try {
153-
const handlerProtos = this.eventHandlerFactory.getHandlerProtos(event);
154-
await Promise.all(
155-
handlerProtos.map(async (proto) => {
156-
try {
157-
await this.eventHandlerFactory.handle(event, proto, args);
158-
} catch (e: any) {
159-
// should wait all handlers done then destroy ctx
160-
e.message = `[EventBus] process event ${String(event)} for handler ${String(proto.name)} failed: ${e.message}`;
153+
const bag = this.teggScopeBag ?? TeggScope.current();
154+
const doRun = () =>
155+
ContextHandler.run(ctx, async () => {
156+
const lifecycle = {};
157+
if (ctx.init) {
158+
await ctx.init(lifecycle);
159+
}
160+
try {
161+
const handlerProtos = this.eventHandlerFactory.getHandlerProtos(event);
162+
await Promise.all(
163+
handlerProtos.map(async (proto) => {
164+
try {
165+
await this.eventHandlerFactory.handle(event, proto, args);
166+
} catch (e: any) {
167+
// should wait all handlers done then destroy ctx
168+
e.message = `[EventBus] process event ${String(event)} for handler ${String(proto.name)} failed: ${e.message}`;
169+
this.logger.error(e);
170+
}
171+
}),
172+
);
173+
} catch (e: any) {
174+
e.message = `[EventBus] process event ${String(event)} failed: ${e.message}`;
175+
this.logger.error(e);
176+
} finally {
177+
if (ctx.destroy) {
178+
ctx.destroy(lifecycle).catch((e) => {
179+
e.message = '[tegg/SingletonEventBus] destroy tegg ctx failed:' + e.message;
161180
this.logger.error(e);
162-
}
163-
}),
164-
);
165-
} catch (e: any) {
166-
e.message = `[EventBus] process event ${String(event)} failed: ${e.message}`;
167-
this.logger.error(e);
168-
} finally {
169-
if (ctx.destroy) {
170-
ctx.destroy(lifecycle).catch((e) => {
171-
e.message = '[tegg/SingletonEventBus] destroy tegg ctx failed:' + e.message;
172-
this.logger.error(e);
173-
});
181+
});
182+
}
174183
}
175-
}
176-
this.doOnceEmit(event, args);
177-
});
184+
this.doOnceEmit(event, args);
185+
});
186+
await TeggScope.runMaybe(bag, doRun);
178187
}
179188
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { LifecycleContext, LifecycleObject } from '@eggjs/tegg-types';
2+
import { TeggScope, type TeggScopeBag } from '@eggjs/tegg-types';
3+
4+
import { LifecycleUtil } from './LifycycleUtil.ts';
5+
6+
/**
7+
* Get-or-create the concrete per-app {@link LifecycleUtil} stored at `slot` in a
8+
* specific bag. Used to **pin** a lifecycle util to a known app's bag
9+
* (`app._teggScopeBag`) without depending on the active async scope — e.g. the
10+
* `app.xxxLifecycleUtil` facades, so plugins can register hooks during boot
11+
* WITHOUT wrapping every call in `TeggScope.run(...)`.
12+
*/
13+
export function lifecycleUtilFromBag<T extends LifecycleContext, R extends LifecycleObject<T>>(
14+
bag: TeggScopeBag,
15+
slot: symbol,
16+
): LifecycleUtil<T, R> {
17+
let util = bag.get(slot) as LifecycleUtil<T, R> | undefined;
18+
if (!util) {
19+
util = new LifecycleUtil<T, R>();
20+
bag.set(slot, util);
21+
}
22+
return util;
23+
}
24+
25+
/**
26+
* Create a per-app {@link LifecycleUtil} facade backed by {@link TeggScope}.
27+
*
28+
* The returned object has the same shape/type as a `LifecycleUtil`, but every
29+
* method resolves the per-app instance from the active {@link TeggScope} bag (the
30+
* SAME instance {@link lifecycleUtilFromBag} returns for that bag). This lets
31+
* module-level lifecycle-util singletons (e.g. `EggPrototypeLifecycleUtil`)
32+
* become per-app WITHOUT changing any call site, so hooks fired deep in
33+
* metadata/runtime (where there is no `app` reference) hit the current app's util.
34+
*
35+
* It is an explicit delegating object (NOT a Proxy) — each call is one slot
36+
* resolve + a direct method call, with no per-access trap or bound-function
37+
* allocation.
38+
*/
39+
export function createScopedLifecycleUtil<T extends LifecycleContext, R extends LifecycleObject<T>>(
40+
slot: symbol,
41+
desc: string,
42+
): LifecycleUtil<T, R> {
43+
const get = (): LifecycleUtil<T, R> => TeggScope.resolve(slot, () => new LifecycleUtil<T, R>(), desc);
44+
const facade: Pick<
45+
LifecycleUtil<T, R>,
46+
| 'registerLifecycle'
47+
| 'deleteLifecycle'
48+
| 'getLifecycleList'
49+
| 'registerObjectLifecycle'
50+
| 'deleteObjectLifecycle'
51+
| 'clearObjectLifecycle'
52+
| 'getObjectLifecycleList'
53+
| 'objectPreCreate'
54+
| 'objectPostCreate'
55+
| 'objectPreDestroy'
56+
| 'getLifecycleHook'
57+
> = {
58+
registerLifecycle: (lifecycle) => get().registerLifecycle(lifecycle),
59+
deleteLifecycle: (lifecycle) => get().deleteLifecycle(lifecycle),
60+
getLifecycleList: () => get().getLifecycleList(),
61+
registerObjectLifecycle: (obj, lifecycle) => get().registerObjectLifecycle(obj, lifecycle),
62+
deleteObjectLifecycle: (obj, lifecycle) => get().deleteObjectLifecycle(obj, lifecycle),
63+
clearObjectLifecycle: (obj) => get().clearObjectLifecycle(obj),
64+
getObjectLifecycleList: (obj) => get().getObjectLifecycleList(obj),
65+
objectPreCreate: (ctx, obj) => get().objectPreCreate(ctx, obj),
66+
objectPostCreate: (ctx, obj) => get().objectPostCreate(ctx, obj),
67+
objectPreDestroy: (ctx, obj) => get().objectPreDestroy(ctx, obj),
68+
getLifecycleHook: (hookName, proto) => get().getLifecycleHook(hookName, proto),
69+
};
70+
return facade as LifecycleUtil<T, R>;
71+
}

tegg/core/lifecycle/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from '@eggjs/tegg-types/lifecycle';
22

33
export * from './LifycycleUtil.ts';
4+
export * from './ScopedLifecycleUtil.ts';
45
export * from './IdenticalObject.ts';
56
export * from './decorator/index.ts';

tegg/core/lifecycle/test/__snapshots__/index.test.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ exports[`should export stable 1`] = `
1111
"LifecyclePreInject": [Function],
1212
"LifecyclePreLoad": [Function],
1313
"LifecycleUtil": [Function],
14+
"createScopedLifecycleUtil": [Function],
15+
"lifecycleUtilFromBag": [Function],
1416
}
1517
`;

0 commit comments

Comments
 (0)