You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
0 commit comments