Skip to content

feat(tegg): isolate per-app state for concurrent multi-app via TeggScope#5986

Merged
killagu merged 17 commits into
eggjs:nextfrom
killagu:feat/tegg-multiapp-isolation
Jun 27, 2026
Merged

feat(tegg): isolate per-app state for concurrent multi-app via TeggScope#5986
killagu merged 17 commits into
eggjs:nextfrom
killagu:feat/tegg-multiapp-isolation

Conversation

@killagu

@killagu killagu commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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 statePrototypeUtil.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

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.

Allow multiple tegg apps to boot and serve requests concurrently in one
process without cross-talk. Introduce a low-level, type-free `TeggScope`
(AsyncLocalStorage<Map<symbol, unknown>>) in @eggjs/tegg-types. Each package
backs its process-global statics with a per-app scope slot + fallback, so the
static facade call sites stay unchanged; boot hooks, request middleware,
unittest scope, standalone Runner, and escape points wrap work in
`TeggScope.run(app._teggScopeBag, ...)`.

Core
- TeggScope keystone: current()/run()/createBag()/registerScope() + a single
  unified process-default bag for all no-scope fallback, plus a strict-mode
  escape fuse (dev throw / prod warn) that arms only under true multi-app
  (explicitScopeCount > 1); single-app stays silent.
- metadata: EggPrototypeFactory.instance, GlobalGraph.instance, LoadUnitFactory
  maps (two-tier creator map: shared base for import-time creators + per-app
  overlay for boot-time app-capturing creators), per-app class->proto map
  (getPrototypeByClazz) to fix concurrent getEggObject(clazz) resolution.
- runtime: LoadUnitInstanceFactory.instanceMap, EggObjectFactory.eggObjectMap,
  ContextHandler callbacks, all five lifecycle utils become per-app via a
  createScopedLifecycleUtil proxy in @eggjs/lifecycle.
- common-util: ModuleConfigUtil.configNames per-app (fixes the standalone
  config-names race).

Plugins / standalone
- plugin/tegg: build per-app bag in configWillLoad, wrap boot/request/unittest
  + getEggObject + app.module/ctx.module proxies; per-app CompatibleUtil caches.
- controller/aop/dal/eventbus/orm/schedule/langchain: wrap lifecycle-hook
  registration in the app scope (per-app lifecycle utils require it).
- dal: per-app TableModel/MysqlDataSource/SqlMap managers; app extend getter
  pins to the app scope.
- eventbus: SingletonEventBus captures its app bag and re-establishes it in
  doEmit (detached fire-and-forget dispatch isolation).
- controller MCP + mcp-proxy: per-app register/hooks; transport captures its
  register; addHook runs in scope.
- standalone Runner: per-Runner bag wrapping load/init/run/destroy.

Tests
- New plugin/tegg MultiApp.test.ts: two concurrent apps sharing one module —
  isolated singletons, per-app data store, EventBus emit/handler dispatch,
  background tasks, and sequential no-leak, all under strict mode.
- Update additive export-stable snapshots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds TeggScope for per-app async scoping, rewires core registries and lifecycle utilities to use scoped bags, updates plugin boot and standalone flows to run inside app-owned scope, and adds multi-app isolation tests and fixtures.

Changes

Multi-App Isolation and Scoped Runtime

Layer / File(s) Summary
Scope API and guidance
AGENTS.md, tegg/CLAUDE.md, tegg/core/types/package.json, tegg/core/types/src/index.ts, tegg/core/types/src/scope/*, tegg/core/types/test/TeggScope.test.ts
Adds TeggScope, its public exports, scope guidance docs, and direct scope tests for the new bag API.
Scoped lifecycle utilities
tegg/core/lifecycle/src/*, tegg/core/metadata/src/model/EggPrototype.ts, tegg/core/metadata/src/model/LoadUnit.ts, tegg/core/runtime/src/model/EggContext.ts, tegg/core/runtime/src/model/EggObject.ts, tegg/core/runtime/src/model/LoadUnitInstance.ts
Replaces direct LifecycleUtil instances with scoped facades and bag-based helpers for egg and load-unit lifecycle utilities.
Core registries and lookups
tegg/core/common-util/src/ModuleConfig.ts, tegg/core/metadata/src/factory/*, tegg/core/metadata/src/impl/LoadUnitMultiInstanceProtoHook.ts, tegg/core/metadata/src/model/graph/GlobalGraph.ts, tegg/core/aop-runtime/src/LoadUnitAopHook.ts, tegg/core/dynamic-inject-runtime/src/EggObjectFactory.ts, tegg/core/runtime/src/factory/*, tegg/core/runtime/src/model/ContextHandler.ts, tegg/core/eventbus-runtime/src/SingletonEventBus.ts
Moves shared factories, graph state, module config, and event dispatch to TeggScope-resolved storage and app-bag lookups.
TEgg app scope wiring
tegg/plugin/tegg/src/*, tegg/plugin/tegg/src/app/extend/*, tegg/plugin/tegg/src/lib/*, tegg/plugin/eventbus/src/*, tegg/plugin/aop/src/app.ts, tegg/plugin/langchain/src/*
Runs TEgg boot, middleware, and plugin lookups inside the app bag and resolves egg objects through scoped calls.
Plugin boot integrations
tegg/plugin/config/*, tegg/plugin/controller/src/**/*, tegg/plugin/dal/src/**/*, tegg/plugin/dal/test/transaction.test.ts, tegg/plugin/orm/*, tegg/plugin/mcp-proxy/src/app.ts, tegg/plugin/mcp-proxy/src/index.ts, tegg/plugin/schedule/src/app.ts
Scopes controller, config, DAL, ORM, MCP proxy, and schedule lifecycle hooks and manager singletons to the active app.
Standalone runner scope
tegg/standalone/standalone/package.json, tegg/standalone/standalone/src/*
Runs standalone preload, init, run, and destroy flows inside a runner-owned TeggScope bag.
Multi-app tests and fixtures
tegg/plugin/tegg/package.json, tegg/plugin/tegg/test/MultiApp*.test.ts, tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/..., tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation-b/...
Adds multi-app regression suites and fixture apps that exercise singleton, eventbus, background task, and restart isolation.

Sequence Diagram(s)

sequenceDiagram
  participant TeggAppBoot
  participant TeggScope
  participant TEggPluginApplication
  participant EggContainerFactory
  participant EggPrototypeFactory

  TeggAppBoot->>TeggScope: registerScope(app._teggScopeBag)
  TeggAppBoot->>TeggScope: run(app._teggScopeBag, lifecycle work)
  TEggPluginApplication->>TeggScope: runMaybe(app._teggScopeBag, doWork)
  TeggScope->>EggContainerFactory: execute doWork in app bag
  EggContainerFactory->>EggPrototypeFactory: getPrototypeByClazzOrGlobal(clazz)
  EggPrototypeFactory-->>EggContainerFactory: scoped prototype
  EggContainerFactory-->>TEggPluginApplication: egg object
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • eggjs/egg#5846: Also touches tegg/plugin/config/src/app.ts and the same module-config boot flow that now runs inside TeggScope.runMaybe.
  • eggjs/egg#5993: Also touches tegg/plugin/aop/src/app.ts and nearby GlobalGraph-related boot timing in the same lifecycle path.

Suggested reviewers

  • jerryliang64

Poem

🐇 I hopped through scopes with careful feet,
Each app kept carrots neat and sweet.
No shared burrow leaks from side to side,
Just bagged-up hops and rabbit pride.
✨ Thump-thump!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: isolating per-app state with TeggScope for concurrent multi-app execution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.36646% with 90 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.92%. Comparing base (753e045) to head (84d4762).
⚠️ Report is 7 commits behind head on next.

Files with missing lines Patch % Lines
...plugin/tegg/src/app/extend/application.unittest.ts 2.85% 28 Missing and 6 partials ⚠️
tegg/plugin/tegg/src/app/extend/context.ts 0.00% 12 Missing ⚠️
tegg/plugin/tegg/src/app/extend/application.ts 26.66% 11 Missing ⚠️
tegg/standalone/standalone/src/Runner.ts 88.88% 6 Missing ⚠️
...gg/plugin/tegg/src/lib/ctx_lifecycle_middleware.ts 77.27% 5 Missing ⚠️
tegg/plugin/dal/src/app.ts 0.00% 4 Missing ⚠️
...egg/core/eventbus-runtime/src/SingletonEventBus.ts 86.36% 3 Missing ⚠️
tegg/plugin/config/src/app.ts 76.92% 3 Missing ⚠️
tegg/core/lifecycle/src/ScopedLifecycleUtil.ts 90.47% 2 Missing ⚠️
tegg/core/runtime/src/model/ContextHandler.ts 87.50% 2 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5986      +/-   ##
==========================================
+ Coverage   84.89%   84.92%   +0.03%     
==========================================
  Files         669      676       +7     
  Lines       19942    20513     +571     
  Branches     3964     4058      +94     
==========================================
+ Hits        16929    17420     +491     
- Misses       2588     2664      +76     
- Partials      425      429       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@socket-security

socket-security Bot commented Jun 23, 2026

Copy link
Copy Markdown

Dependency limit exceeded — report not shown.

This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report.

Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard.

Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces TeggScope to provide per-app isolation for singletons, caches, and registries, resolving concurrency issues in multi-app environments where process-global statics would otherwise conflict. Key changes include wrapping lifecycle utilities in a scope-backed proxy via createScopedLifecycleUtil, refactoring core factories and registers to resolve instances from the active TeggScope bag, and updating plugins (AOP, Controller, DAL, EventBus, LangChain, ORM, Schedule) to execute boot and request-handling flows within the appropriate app scope. The review feedback highlights several opportunities to refine this implementation, such as caching bound methods in the lifecycle utility proxy to preserve referential equality, adding escape checks to TeggScope.set under multi-app mode, removing redundant legacy fallbacks in static setters, and defensively checking for the scope bag in context extensions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tegg/core/lifecycle/src/ScopedLifecycleUtil.ts Outdated
Comment thread tegg/core/types/src/scope/TeggScope.ts
Comment thread tegg/core/metadata/src/model/graph/GlobalGraph.ts Outdated
Comment thread tegg/plugin/controller/src/lib/impl/http/HTTPControllerRegister.ts Outdated
Comment thread tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts Outdated
Comment thread tegg/plugin/tegg/src/app/extend/context.ts
killagu and others added 12 commits June 23, 2026 11:13
Add a "Multi-App Isolation (TeggScope)" section to tegg/CLAUDE.md with the rules
agents/devs must follow when editing tegg core/plugins: no new process-global
mutable runtime state (back per-app state with a TeggScope slot), wrap plugin
lifecycle-hook registration in TeggScope.run(app._teggScopeBag, ...), resolve
egg objects per-app, handle detached escape points, and the strict-mode fuse.
Also note the (negligible) performance characteristics. Add a concise pointer
rule under Coding Conventions in AGENTS.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduce the per-app isolation overhead and the boilerplate `TeggScope.run(...)`
wrapping, following review feedback.

Convergence (less defensive boilerplate):
- `app.*LifecycleUtil` getters are now PINNED to the app's scope bag via new
  `xxxLifecycleUtilFromBag(bag)` helpers, so plugin boot hooks register lifecycle
  hooks WITHOUT wrapping each call in `TeggScope.run`. Removed the run wraps from
  aop/dal/eventbus/orm/schedule/langchain boot hooks (kept only where a scope is
  genuinely required: moduleHandler.init/destroy, sdk-base construction,
  leoricRegister, registerLoadUnitCreator overlay, request/getEggObject paths,
  DAL manager teardown, standalone Runner).
- `GlobalGraph.instanceFor(bag)` lets aop/controller register build hooks pinned
  to the owning app's graph (no run wrap).

Performance:
- lifecycle utils: replace the per-access Proxy facade with an explicit
  delegating object (no get-trap / bound-function allocation): ~27ns -> ~8ns per
  lifecycle-util call.
- `TeggScope.resolve`/`getOr`: single `getStore()` per call; the in-scope common
  path never reaches the escape check, and single-app never escapes.

Correctness (latent multi-app bugs exposed by the convergence):
- Resolve egg objects by class via the per-app `EggPrototypeFactory.getPrototypeByClazz`
  (falling back to the global `PrototypeUtil.getClazzProto`) in EggContextEventBus,
  dynamic-inject EggObjectFactory and aop LoadUnitAopHook — the global class->proto
  map is shared and overwritten by concurrent apps.

Docs: document the TeggScope constraints + performance notes in tegg/CLAUDE.md
(added previously) still apply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	tegg/plugin/aop/src/app.ts
#	tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts
The aop and eventbus plugins declared @eggjs/tegg-types in dependencies but
never import it (aop resolves the per-app graph via app._teggScopeBag and
GlobalGraph from @eggjs/metadata; eventbus scopes via app.getEggObject /
eggPrototypeFactory.getPrototypeByClazz). tsdown's unplugin-unused fails the
build with 'Unused 1 dependencies found', which broke the cnpmcore/examples
E2E build step. Remove the dead dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-app state (factories, GlobalGraph, the ContextHandler request-context
callbacks, …) is installed into app._teggScopeBag at boot. But code that runs
OUTSIDE an explicit TeggScope.run — a SingletonProto method called directly, a
detached leoric SQL logger, a ContextProto method invoked in a test body —
resolved the SEPARATE process-default bag, which never held that state. Boot
state was therefore invisible to single-app call sites that did not go through a
wrapped entry point.

Pre-scoping these were process-global statics, so they were reachable from
anywhere after boot. Restore that for single-app while keeping multi-app
isolation: when EXACTLY ONE app is alive, the no-scope fallback now resolves to
that app's bag (TeggScope tracks live scope bags in a Set instead of a bare
counter). Zero apps (pure core unit tests / standalone helpers) or true
multi-app keep using the process-default bag, and multi-app out-of-scope access
still trips the escape fuse.

Also make ContextHandler.getContext() return undefined instead of asserting when
no callback is installed: 'no active context' is a valid read result (e.g. a
singleton service or detached logger called with no request in flight). Multi-app
escape detection is handled by the TeggScope fuse, not this assert.

Fixes the @eggjs/orm-plugin regressions where leoric's SQL logger threw
'getContextCallback not set' (swallowed by leoric) so the SQL line was never
logged, and the context-proto tests saw an undefined ctx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- TeggScope.set: report an escape when it writes to the process-default bag
  under multi-app with no active scope, matching resolve()/getOr() so a missed
  scope wrap that mutates shared state is caught.
- GlobalGraph / HTTPControllerRegister / MCPControllerRegister: drop the dead
  #legacyInstance fallback. TeggScope.set always returns true and writes to the
  active/sole-app/default bag, so the 'if (!set(...)) #legacyInstance = value'
  branch never ran and the getter's legacy() was always undefined. The bag is
  the single source of truth; simplify the getter/setter accordingly.
- context.ts getEggObject/getEggObjectFromName: guard app._teggScopeBag before
  TeggScope.run, consistent with application.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add unit coverage for the multi-app scope primitive: run/current, live-scope
tracking + isMultiApp, resolve memoization, the no-scope fallbacks (zero apps ->
process-default bag; exactly one app -> that app's bag so out-of-scope access
sees boot state), getOr legacy fallback, set, and the strict-mode escape fuse
that throws on out-of-scope access under multi-app. Regression coverage for the
single-app-state fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavior-preserving simplifications across the multi-app PR (all verified by the
full tegg suite):

- TeggScope: add runMaybe(bag, fn) and route the 8 copy-pasted
  `bag ? run(bag, fn) : fn()` sites through it; inline the single-caller
  #activeBag into set() (one getStore, mirrors resolve/getOr); drop the unused
  _setDefaultBag.
- EggPrototypeFactory.getPrototypeByClazzOrGlobal: centralize the rule-4
  `getPrototypeByClazz(clazz) ?? PrototypeUtil.getClazzProto(clazz)` fallback so
  the 4 call sites (aop-runtime, dynamic-inject-runtime, runtime, eventbus) stop
  re-implementing it; drop the now-unused PrototypeUtil imports/casts.
- lifecycle: add defineScopedLifecycleUtil(slot, desc) returning [util, fromBag]
  and collapse the repeated declare-slot + createScopedLifecycleUtil +
  lifecycleUtilFromBag boilerplate in the 5 model files.
- EggPrototypeFactory: use the canonical EggPrototypeWithClazz cast instead of an
  ad-hoc `as unknown as { clazz }`.
- controller: drop the dead willReady MCP block (connectStatelessStreamTransport
  is a no-op; transports are per-request now) and its scope wrap; remove the
  redundant ControllerMetadataManager constructor and a self-nested csrf
  Array.isArray re-test.
- standalone Runner: one uniform runInScope() helper across all 5 scope entries.
- orm/mcp-proxy: collapse trivial single-statement scope-run closures.

Net -56 lines; no public API or runtime behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three real correctness bugs from an adversarial review of the multi-app PR:

- langchain: GraphLoadUnitHook captured app.eggPrototypeFactory in the boot
  CONSTRUCTOR (runs before any app scope exists), so under concurrent multi-app
  it resolved another live app's factory (cross-app proto pollution) or tripped
  the strict-mode escape fuse on a 3rd app's boot. Resolve EggPrototypeFactory
  .instance lazily inside preCreate, which runs within this app's TeggScope.
- standalone Runner: TeggScope.registerScope ran in the constructor but
  unregisterScope only on destroy()'s success path, leaking the scope into
  liveScopeBags on construction/init/destroy failure — a single leak flips
  isMultiApp on and breaks the sole-app fallback + escape fuse for every later
  Runner. Now: destroy() unregisters in a finally, the constructor unregisters
  on failure, and main() tears the Runner down when init() throws.
- plugin/tegg beforeClose: moved unregisterScope into a finally so a throwing
  moduleHandler.destroy() no longer leaks the app scope.

All verified by the tegg suite (langchain/standalone/tegg + core/plugins green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ations)

`export const [Util, utilFromBag] = defineScopedLifecycleUtil(...)` (a binding
pattern) is rejected by the repo's --isolatedDeclarations d.ts emit (TS9019),
breaking the build + both E2E suites. Restore the explicit
`export const Util: LifecycleUtil<...> = createScopedLifecycleUtil(...)` +
`export function utilFromBag(...)` form in the 5 model files and drop the unused
defineScopedLifecycleUtil helper. The other simplifications are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
teggConfig boots before the tegg plugin (tegg depends on teggConfig), so it is
the first tegg lifecycle to read/write the per-app scoped `configNames`. The
tegg plugin only creates `app._teggScopeBag` in its own `configWillLoad` — too
late for teggConfig's `#loadModuleConfigs` read. Under concurrent multi-app boot
that read escaped to the process-default bag once a second app was live,
tripping the strict-mode fuse.

teggConfig now creates `app._teggScopeBag` if absent and runs its configNames
set/read/clear inside `TeggScope.run`; the tegg plugin reuses the bag via `??=`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add MultiAppParallel.test.ts: `describe.concurrent` boots several `mm.app`
instances at once (`cache: false` so same-baseDir bodies get distinct apps),
each asserting per-app isolation of its singleton, data store and eventbus
dispatch. Unlike MultiApp.test.ts (two different baseDirs driven sequentially),
this reproduces same-baseDir concurrent boot, which surfaced the teggConfig
configNames scope escape fixed in the previous commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@killagu

killagu commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

Update: concurrent-boot regression test + a scope fix it surfaced

Review feedback. All six inline points from the automated review are already implemented in the current revision (the simplification + adversarial-review passes) — replied inline and resolved each thread:

  • TeggScope.set escape check ✓ (present)
  • GlobalGraph / HTTPControllerRegister / MCPControllerRegister#legacyInstance removed, getters/setters delegate straight to TeggScope.getOr/set
  • ScopedLifecycleUtil is no longer a Proxy — explicit delegating object, methods allocated once (no per-get bound-fn) ✓
  • context.ts getEggObject* use TeggScope.runMaybe(bag, …) (the defensive bag guard) ✓

New test — MultiAppParallel.test.ts. MultiApp.test.ts drives two different-baseDir apps sequentially inside one it. The new file uses describe.concurrent to boot several mm.app instances at the same time (cache: false so same-baseDir bodies get distinct apps), each asserting isolation of its singleton / data-store / eventbus dispatch — matching how the suite actually runs under the repo's vitest config (pool: 'threads', isolate: false).

Bug it caught + fixed. Under concurrent same-baseDir boot the strict-mode fuse threw escaped to the process-default bag (ModuleConfigUtil.configNames): teggConfig boots before the tegg plugin (tegg depends on it), so it reads/writes the per-app scoped configNames before app._teggScopeBag exists, and once a second app is live the read escapes. Fix: teggConfig now creates the bag if absent and runs its configNames set/read/clear inside TeggScope.run; the tegg plugin reuses the bag via ??=.

Verified: parallel test stable across repeats; full plugin + standalone + config suite single-worker (--no-file-parallelism, max shared-state) = 192 passed, 0 escapes; typecheck / oxlint / oxfmt clean.

@killagu killagu marked this pull request as ready for review June 27, 2026 11:28
Copilot AI review requested due to automatic review settings June 27, 2026 11:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (1)
tegg/plugin/tegg/test/MultiApp.test.ts (1)

11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace timer polling with the repository async-test helper.

This introduces setTimeout polling in a tegg/**/*.test.ts file, which the repo explicitly avoids for async background work. Please switch this wait path to the existing helper pattern used in tegg tests.

As per coding guidelines, tegg/**/*.test.{ts,tsx}: Use BackgroundTaskHelper instead of setTimeout or setImmediate in tests for async background work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tegg/plugin/tegg/test/MultiApp.test.ts` around lines 11 - 18, The waitFor
helper in MultiApp.test.ts uses manual setTimeout polling, which violates the
tegg test async-pattern guidelines. Replace this polling loop with the
repository’s BackgroundTaskHelper-based helper pattern used in other tegg tests,
and update the test flow to wait through that helper instead of repeatedly
checking predicate with setTimeout.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Around line 49-53: Narrow the tegg multi-app isolation summary so it only
applies to module-level lifecycle util access, since the current blanket
requirement conflicts with the bag-pinned `ScopedLifecycleUtil` and plugin boot
flow. Update the wording in the `tegg` guidance to say registrations using
`app.*LifecycleUtil` do not need an extra outer `TeggScope.run(...)` wrap when
they are already scoped to `app._teggScopeBag`, while keeping the
no-process-global-state rule intact.

In `@tegg/CLAUDE.md`:
- Around line 327-334: Update Rule 3 and the performance note to reflect the
shipped scoped facade instead of the old per-call TeggScope.run pattern. In the
CLAUDE.md guidance, revise the lifecycle-hook registration instructions so
plugin boot code uses the app-scoped lifecycle util getters exposed by
ScopedLifecycleUtil, which already delegate correctly, and remove language
implying every registerLifecycle/deleteLifecycle call must be wrapped manually.
Keep the references aligned with ScopedLifecycleUtil and the plugin boot
lifecycle registration examples so future changes follow the current design.

In `@tegg/core/types/src/scope/TeggScope.ts`:
- Around line 54-65: Fail closed for multi-app scope escapes in TeggScope:
reportEscape() currently only warns in production, allowing resolve/getOr/set to
continue into the process-default bag and mix app state. Update the
escape-handling path in TeggScope so that a missing active scope always aborts
the operation (for example by throwing or otherwise preventing fallback) and
make resolve, getOr, and set respect that failure instead of proceeding to the
default bag.

In `@tegg/plugin/dal/src/lib/MysqlDataSourceManager.ts`:
- Around line 9-17: The per-app teardown in MysqlDataSourceManager is incomplete
because MysqlDataSourceManager.instance.clear() only resets dataSourceIndices
while leaving dataSources populated. Update clear() in MysqlDataSourceManager to
also release the datasource cache, and if MysqlDataSource provides a
close/shutdown method, invoke it for each stored datasource before dropping the
map. Keep the fix localized to MysqlDataSourceManager so the app-close path in
app.ts באמת fully disposes per-app resources.

In `@tegg/plugin/mcp-proxy/src/app.ts`:
- Around line 15-16: The app-scoped hook registration is correct, but the later
MCPProxyHook callbacks read MCPControllerRegister.hooks outside the app’s
TeggScope, so they can see the wrong hook list. Capture the current app bag in
tegg/plugin/mcp-proxy/src/app.ts when registering the hook, and re-enter that
bag with TeggScope.run inside the proxy-handler callbacks before accessing
MCPControllerRegister.hooks. Use the existing symbols TeggScope,
MCPControllerRegister, and MCPProxyHook to locate the callback paths that need
wrapping.

In `@tegg/plugin/tegg/src/app/extend/application.unittest.ts`:
- Around line 17-29: The mock-context guard is still process-global, so parallel
apps can interfere with each other even though the helpers run in the app bag.
Move the hasMockModuleContext state into a TeggScope slot and update the related
helpers in mockModuleContextScope and destroyModuleContext to read/write through
that scoped getter/setter instead of a shared variable. Keep the existing
app-scoped flow in application.unittest.ts and ensure all guard checks use the
scoped value consistently.

In
`@tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/modules/counter-module/BackgroundCounterService.ts`:
- Around line 18-24: The schedule method in BackgroundCounterService is still
executing the work synchronously because the callback passed to
BackgroundTaskHelper.run does not introduce an async boundary before using
counterService. Update the callback so it truly defers execution across an
awaited boundary before incrementing, and keep the logic inside schedule using
the existing BackgroundTaskHelper.run and counterService symbols so the test
exercises background-scoped resolution after an async escape.

In `@tegg/plugin/tegg/test/MultiApp.test.ts`:
- Around line 106-123: In the “should not leak state between sequential app
lifecycles” test, the first app instance is only closed after the assertions, so
a failure before that can leave app1 running and affect later tests. Update the
test flow around mm.app, app1.ready(), and CounterService usage so app1 is
always closed in a finally block, keeping the existing app2 cleanup pattern and
ensuring both app lifecycles are isolated even when an assertion fails.

In `@tegg/plugin/tegg/test/MultiAppParallel.test.ts`:
- Around line 10-18: Replace the raw timer-based polling and yield in
MultiAppParallel.test with BackgroundTaskHelper so the test waits on background
work deterministically instead of relying on setTimeout/setImmediate. Update the
waitFor helper and the explicit async pause to use BackgroundTaskHelper, keeping
the test synchronized through the helper APIs rather than time-based retries.
Make sure the changes are applied wherever the test currently uses those waits
so the concurrency check remains stable under load.

In `@tegg/standalone/standalone/src/EggModuleLoader.ts`:
- Around line 72-79: `EggModuleLoader.generateAppGraph` builds
`moduleConfigList`, but `loaderCache` is never populated before
`loaderCache.get(modulePath)!` is used, so
`LoadUnitFactory.createPreloadLoadUnit` receives an invalid loader. Update the
preload loop in `EggModuleLoader` to fill `loaderCache` with the correct
`Loader` instances for each `modulePath` before lookup, or derive the loader
directly from the available module metadata, and keep the `moduleConfig.path` /
`LoadUnitFactory.createPreloadLoadUnit` flow consistent.

In `@tegg/standalone/standalone/src/Runner.ts`:
- Around line 292-296: The Runner prototype resolution currently goes straight
to the global metadata lookup, which can ignore a per-scope registration. Update
the prototype lookup in Runner’s constructor/setup path to try
EggPrototypeFactory.instance.getPrototypeByClazz(runnerClass) first, and only
fall back to PrototypeUtil.getClazzProto(runnerClass) if the scoped lookup
returns nothing. Keep the existing error handling and assignment to
this.runnerProto, but make sure the scoped factory lookup is the primary path
used for runnerClass.

---

Nitpick comments:
In `@tegg/plugin/tegg/test/MultiApp.test.ts`:
- Around line 11-18: The waitFor helper in MultiApp.test.ts uses manual
setTimeout polling, which violates the tegg test async-pattern guidelines.
Replace this polling loop with the repository’s BackgroundTaskHelper-based
helper pattern used in other tegg tests, and update the test flow to wait
through that helper instead of repeatedly checking predicate with setTimeout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eb36df64-cd55-4ff5-bb2a-1faa82185fbb

📥 Commits

Reviewing files that changed from the base of the PR and between 753e045 and b87a2b8.

⛔ Files ignored due to path filters (6)
  • tegg/core/lifecycle/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • tegg/core/metadata/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • tegg/core/runtime/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • tegg/core/tegg/test/__snapshots__/exports.test.ts.snap is excluded by !**/*.snap
  • tegg/core/tegg/test/__snapshots__/helper.test.ts.snap is excluded by !**/*.snap
  • tegg/core/types/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (73)
  • AGENTS.md
  • tegg/CLAUDE.md
  • tegg/core/aop-runtime/src/LoadUnitAopHook.ts
  • tegg/core/common-util/src/ModuleConfig.ts
  • tegg/core/dynamic-inject-runtime/src/EggObjectFactory.ts
  • tegg/core/eventbus-runtime/src/SingletonEventBus.ts
  • tegg/core/lifecycle/src/ScopedLifecycleUtil.ts
  • tegg/core/lifecycle/src/index.ts
  • tegg/core/metadata/src/factory/EggPrototypeFactory.ts
  • tegg/core/metadata/src/factory/LoadUnitFactory.ts
  • tegg/core/metadata/src/impl/LoadUnitMultiInstanceProtoHook.ts
  • tegg/core/metadata/src/model/EggPrototype.ts
  • tegg/core/metadata/src/model/LoadUnit.ts
  • tegg/core/metadata/src/model/graph/GlobalGraph.ts
  • tegg/core/runtime/src/factory/EggContainerFactory.ts
  • tegg/core/runtime/src/factory/EggObjectFactory.ts
  • tegg/core/runtime/src/factory/LoadUnitInstanceFactory.ts
  • tegg/core/runtime/src/model/ContextHandler.ts
  • tegg/core/runtime/src/model/EggContext.ts
  • tegg/core/runtime/src/model/EggObject.ts
  • tegg/core/runtime/src/model/LoadUnitInstance.ts
  • tegg/core/types/package.json
  • tegg/core/types/src/index.ts
  • tegg/core/types/src/scope/TeggScope.ts
  • tegg/core/types/src/scope/index.ts
  • tegg/core/types/test/TeggScope.test.ts
  • tegg/plugin/aop/src/app.ts
  • tegg/plugin/config/package.json
  • tegg/plugin/config/src/app.ts
  • tegg/plugin/controller/src/app.ts
  • tegg/plugin/controller/src/lib/ControllerMetadataManager.ts
  • tegg/plugin/controller/src/lib/impl/http/HTTPControllerRegister.ts
  • tegg/plugin/controller/src/lib/impl/mcp/MCPControllerRegister.ts
  • tegg/plugin/dal/src/app.ts
  • tegg/plugin/dal/src/app/extend/application.ts
  • tegg/plugin/dal/src/lib/MysqlDataSourceManager.ts
  • tegg/plugin/dal/src/lib/SqlMapManager.ts
  • tegg/plugin/dal/src/lib/TableModelManager.ts
  • tegg/plugin/dal/test/transaction.test.ts
  • tegg/plugin/eventbus/src/app.ts
  • tegg/plugin/eventbus/src/lib/EggContextEventBus.ts
  • tegg/plugin/langchain/src/app.ts
  • tegg/plugin/langchain/src/lib/graph/GraphLoadUnitHook.ts
  • tegg/plugin/mcp-proxy/src/app.ts
  • tegg/plugin/orm/package.json
  • tegg/plugin/orm/src/app.ts
  • tegg/plugin/schedule/src/app.ts
  • tegg/plugin/tegg/package.json
  • tegg/plugin/tegg/src/app.ts
  • tegg/plugin/tegg/src/app/extend/application.ts
  • tegg/plugin/tegg/src/app/extend/application.unittest.ts
  • tegg/plugin/tegg/src/app/extend/context.ts
  • tegg/plugin/tegg/src/lib/CompatibleUtil.ts
  • tegg/plugin/tegg/src/lib/ctx_lifecycle_middleware.ts
  • tegg/plugin/tegg/src/types.ts
  • tegg/plugin/tegg/test/MultiApp.test.ts
  • tegg/plugin/tegg/test/MultiAppParallel.test.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation-b/config/config.default.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation-b/config/module.json
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation-b/config/plugin.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation-b/package.json
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/config/config.default.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/config/module.json
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/config/plugin.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/modules/counter-module/BackgroundCounterService.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/modules/counter-module/CounterEvent.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/modules/counter-module/CounterService.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/modules/counter-module/package.json
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/package.json
  • tegg/standalone/standalone/package.json
  • tegg/standalone/standalone/src/EggModuleLoader.ts
  • tegg/standalone/standalone/src/Runner.ts
  • tegg/standalone/standalone/src/main.ts

Comment thread AGENTS.md
Comment thread tegg/CLAUDE.md Outdated
Comment thread tegg/core/types/src/scope/TeggScope.ts
Comment thread tegg/plugin/dal/src/lib/MysqlDataSourceManager.ts
Comment thread tegg/plugin/mcp-proxy/src/app.ts
Comment thread tegg/plugin/tegg/test/MultiApp.test.ts
Comment thread tegg/plugin/tegg/test/MultiAppParallel.test.ts
Comment thread tegg/standalone/standalone/src/EggModuleLoader.ts Outdated
Comment thread tegg/standalone/standalone/src/Runner.ts Outdated
killagu and others added 4 commits June 27, 2026 21:20
- EggModuleLoader.preLoad built a `loaderCache` it never populated, so
  `loaderCache.get(modulePath)!` passed `undefined` into createPreloadLoadUnit;
  create the loader inline like `load()` does.
- Runner resolved the main runner proto via the process-global
  `PrototypeUtil.getClazzProto`; use the per-app
  `EggPrototypeFactory.instance.getPrototypeByClazzOrGlobal` so concurrent
  Runners don't read clobbered class metadata.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MysqlDataSourceManager.clear() now also clears the `dataSources` map so a
  closed app releases its datasource objects (the map was left populated).
- mcp-proxy proxy handlers run detached from the request; re-enter the owning
  app's TeggScope bag before reading the scope-backed MCPControllerRegister.hooks.
- application.unittest `hasMockModuleContext` was a process-global guard; move it
  into a per-app TeggScope slot so concurrent multi-app tests don't block each
  other.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MultiApp.test.ts: close app1 in a `finally` so an early assertion failure
  cannot leak it into later cases.
- BackgroundCounterService fixture: yield (setImmediate) before incrementing so
  the background-task isolation check truly runs across an async boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
app.*LifecycleUtil getters are bag-pinned, so hook registration through them
needs no TeggScope.run wrap, and the facade is an explicit delegating object,
not a Proxy. Update tegg/CLAUDE.md Rule 3 + the performance note and the
AGENTS.md summary accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
tegg/plugin/tegg/src/app/extend/application.unittest.ts (1)

26-37: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Hold the scoped guard for the full mock-context lifetime.

mockModuleContext sets the guard only after init, mockModuleContextScope never sets it, and destroyModuleContext clears it before destroy completes. Same-app overlapping calls can still pass the guard. Set it immediately after the check, and clear it only in teardown/finally. As per coding guidelines, tegg/**/*.{ts,tsx} runtime state should stay scoped per app; this comment relies on that scoped guard contract.

Suggested guard-lifetime fix
     const doWork = async (): Promise<Context> => {
       if (getHasMockModuleContext()) {
         throw new Error('should not call mockModuleContext twice.');
       }
+      setHasMockModuleContext(true);
       // `@ts-expect-error` mockContext is not typed
       const ctx = this.mockContext(data) as Context;
       const teggCtx = new EggContextImpl(ctx);
       const lifecycle = {};
       TEGG_LIFECYCLE_CACHE.set(teggCtx, lifecycle);
-      if (teggCtx.init) {
-        await teggCtx.init(lifecycle);
+      try {
+        if (teggCtx.init) {
+          await teggCtx.init(lifecycle);
+        }
+        return ctx;
+      } catch (err) {
+        setHasMockModuleContext(false);
+        throw err;
       }
-      setHasMockModuleContext(true);
-      return ctx;
     };
     const doWork = async (): Promise<void> => {
-      setHasMockModuleContext(false);
       const teggCtx = ctx.teggContext;
-      if (!teggCtx) {
-        return;
-      }
-      const lifecycle = TEGG_LIFECYCLE_CACHE.get(teggCtx);
-      if (teggCtx.destroy && lifecycle) {
-        await teggCtx.destroy(lifecycle);
+      try {
+        if (!teggCtx) {
+          return;
+        }
+        const lifecycle = TEGG_LIFECYCLE_CACHE.get(teggCtx);
+        if (teggCtx.destroy && lifecycle) {
+          await teggCtx.destroy(lifecycle);
+        }
+      } finally {
+        setHasMockModuleContext(false);
       }
     };
       if (getHasMockModuleContext()) {
         throw new Error(
           'mockModuleContextScope can not use with mockModuleContext, should use mockModuleContextScope only.',
         );
       }
+      setHasMockModuleContext(true);
       // `@ts-expect-error` mockContextScope only exists in MockApplication
-      return this.mockContextScope(async (ctx: Context) => {
+      return this.mockContextScope(async (ctx: Context) => {
         const teggCtx = new EggContextImpl(ctx);
         const lifecycle = {};
         if (teggCtx.init) {
           await teggCtx.init(lifecycle);
         }
         try {
           return await fn(ctx);
         } finally {
           await teggCtx.destroy(lifecycle);
+          setHasMockModuleContext(false);
         }
       }, data);

Also applies to: 43-55, 60-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tegg/plugin/tegg/src/app/extend/application.unittest.ts` around lines 26 -
37, The scoped guard for mock-context lifecycle is being set too late and
cleared too early, so overlapping calls can still bypass the single-app
constraint. Update the mock-context flow in the application extension methods
around mockModuleContext, mockModuleContextScope, and destroyModuleContext so
setHasMockModuleContext(true) happens immediately after the initial guard check,
and make sure the flag is only cleared in teardown/finally after init/destroy
fully completes. Keep the guard lifetime consistent across the full mock-context
lifetime and preserve the existing TEGG_LIFECYCLE_CACHE and EggContextImpl-based
flow.

Source: Coding guidelines

tegg/plugin/dal/src/lib/MysqlDataSourceManager.ts (1)

60-65: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Close live datasources before clearing the cache.

clear() now releases references, but these entries are described as live datasource connections; dropping the maps may leave pools/sockets open after app teardown. If MysqlDataSource exposes close/destroy/shutdown, call it before clearing and update teardown call sites to await it. This is the remaining part of the earlier teardown concern.

#!/bin/bash
# Verify MysqlDataSource shutdown API and clear() call sites.
rg -nP 'class\s+MysqlDataSource\b|interface\s+MysqlDataSource\b|async\s+(close|destroy|shutdown)\s*\(|\b(close|destroy|shutdown)\s*\(' tegg -g '*.ts' -C3
rg -nP '\bMysqlDataSourceManager\.instance\.clear\s*\(' tegg -g '*.ts' -C3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tegg/plugin/dal/src/lib/MysqlDataSourceManager.ts` around lines 60 - 65,
Close active MysqlDataSource connections before dropping the cache in
MysqlDataSourceManager.clear(), instead of only clearing dataSources and
dataSourceIndices. Check whether MysqlDataSource exposes close, destroy, or
shutdown, and invoke that lifecycle method for each live datasource before
clearing the maps. Then update any MysqlDataSourceManager.instance.clear()
teardown call sites to await the new async clear path so shutdown completes
cleanly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@tegg/plugin/dal/src/lib/MysqlDataSourceManager.ts`:
- Around line 60-65: Close active MysqlDataSource connections before dropping
the cache in MysqlDataSourceManager.clear(), instead of only clearing
dataSources and dataSourceIndices. Check whether MysqlDataSource exposes close,
destroy, or shutdown, and invoke that lifecycle method for each live datasource
before clearing the maps. Then update any
MysqlDataSourceManager.instance.clear() teardown call sites to await the new
async clear path so shutdown completes cleanly.

In `@tegg/plugin/tegg/src/app/extend/application.unittest.ts`:
- Around line 26-37: The scoped guard for mock-context lifecycle is being set
too late and cleared too early, so overlapping calls can still bypass the
single-app constraint. Update the mock-context flow in the application extension
methods around mockModuleContext, mockModuleContextScope, and
destroyModuleContext so setHasMockModuleContext(true) happens immediately after
the initial guard check, and make sure the flag is only cleared in
teardown/finally after init/destroy fully completes. Keep the guard lifetime
consistent across the full mock-context lifetime and preserve the existing
TEGG_LIFECYCLE_CACHE and EggContextImpl-based flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4040eaad-ce6e-497e-bbc2-d4ddfe9dfde7

📥 Commits

Reviewing files that changed from the base of the PR and between b87a2b8 and 84d4762.

📒 Files selected for processing (9)
  • AGENTS.md
  • tegg/CLAUDE.md
  • tegg/plugin/dal/src/lib/MysqlDataSourceManager.ts
  • tegg/plugin/mcp-proxy/src/index.ts
  • tegg/plugin/tegg/src/app/extend/application.unittest.ts
  • tegg/plugin/tegg/test/MultiApp.test.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/modules/counter-module/BackgroundCounterService.ts
  • tegg/standalone/standalone/src/EggModuleLoader.ts
  • tegg/standalone/standalone/src/Runner.ts
✅ Files skipped from review due to trivial changes (2)
  • AGENTS.md
  • tegg/CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • tegg/plugin/tegg/test/MultiApp.test.ts
  • tegg/plugin/tegg/test/fixtures/apps/multi-app-isolation/modules/counter-module/BackgroundCounterService.ts
  • tegg/standalone/standalone/src/EggModuleLoader.ts
  • tegg/standalone/standalone/src/Runner.ts

@killagu killagu merged commit ebfbf52 into eggjs:next Jun 27, 2026
20 of 21 checks passed
killagu added a commit that referenced this pull request Jun 27, 2026
…t gate (#6004)

## Motivation

The suite already runs with `pool: 'threads'` + `isolate: false` for
full parallelism. This PR makes the **effect** of that parallelism
visible in CI — so we can see how parallel the run actually was, per
OS/Node matrix entry — **without changing gate semantics**.

> Independent of #5986 (tegg `TeggScope`): every file touched here is
identical to `next`, so this branch is cut from `next` and can merge on
its own timeline.

## What

- **`vitest.config.ts`** — adds a `json` reporter **only when `CI` is
set**, writing `benchmark/ci-test/ci-run/vitest-results.json` as a side
effect of the gating `ut run ci` (default console reporter preserved; no
extra test run).
- **`.github/workflows/ci.yml`** — a `Report parallelism metrics` step
(`if: always()`) in the existing `test` job summarizes that JSON and
appends the report to the **GitHub job summary**. Gating still comes
solely from `ut run ci`.
- **`scripts/ci-test-benchmark/`** — the existing harness gains a
`--report-only --vitest-json` mode and computes **peak/avg concurrency,
parallel efficiency, and critical path** from a per-file
concurrency-timeline sweep.
- **`test(supertest)`** *(separate commit)* — harden the over-strict
`should handle connection error` test (hardcoded `127.0.0.1:1234`) to
accept the connection-error family instead of exactly `ECONNREFUSED`, so
it isn't environment-dependent (a local proxy on that port made it
`socket hang up`).

## Reading the metrics honestly

Vitest 4 derives a file's interval from **test-level timings only**, so
the spans cover test bodies + per-test `beforeEach`/`afterEach` but
**exclude suite-level `beforeAll`/`afterAll` (where egg boots its apps)
and module transform/import**. Therefore **avg concurrency / parallel
efficiency are lower bounds**; **peak concurrency** is the robust
signal. The worker ceiling mirrors `vitest.config.ts` (Windows CI caps
workers). Fully-skipped files are dropped from the timeline. The report
footnote states all of this.

## Test evidence

- Full **`isolate: false` suite is GREEN** (services up, CI-faithful
`--maxWorkers 4`): **526 files / 3425 tests pass, 0 failures**.
- The edited config loads in real Vitest (both reporters active, JSON at
the configured path); harness math cross-checked independently;
`--report-only` exits `0` on missing JSON; step-summary append is
cross-platform; `oxfmt`/`oxlint` clean.
- A 4-agent adversarial review pass was applied (Windows worker-ceiling,
beforeAll-exclusion docs, skipped-file timeline, report-only run section
all fixed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants