fix: add StandaloneAopContextHook to pre-create ContextProto advice#400
fix: add StandaloneAopContextHook to pre-create ContextProto advice#400jerryliang64 wants to merge 2 commits intoeggjs:masterfrom
Conversation
📝 WalkthroughWalkthroughAdds a StandaloneAopContextHook to pre-create context-scoped AOP advice in standalone mode, wires it into Runner lifecycle for registration and cleanup, exposes the hook via the public index, and includes fixtures and tests demonstrating SingletonProto + ContextProto advice behavior. Changes
Sequence DiagramsequenceDiagram
participant R as Runner
participant LU as LoadUnitInstance
participant H as StandaloneAopContextHook
participant C as EggContext
R->>H: new StandaloneAopContextHook(loadUnitInstances)
H->>LU: scan load units for aspects & advice prototypes
LU-->>H: return aspect/advice metadata
H->>H: validate prototypes, collect CONTEXT advices
R->>C: register H as lifecycle hook (preCreate)
C->>H: preCreate(ctx)
H->>C: addProtoToCreate(name, proto) for each collected advice
R->>H: on destroy -> unregister/cleanup via lifecycle util
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello @jerryliang64, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses a critical runtime issue in the standalone module concerning Aspect-Oriented Programming (AOP). It resolves the 'EggObject not found' error that previously occurred when singleton-scoped objects ( Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a fix for an issue where ContextProto advices were not being created in subsequent requests when used by a SingletonProto in standalone mode. The fix introduces StandaloneAopContextHook, which proactively discovers all context-scoped advices during initialization and ensures they are created with each new context. The implementation is accompanied by a clear test case that reproduces the bug and verifies the fix.
My review focuses on improving the API surface hygiene. I've suggested making a couple of helper interfaces internal to the module as they don't seem to be intended for public consumption.
Overall, this is a well-executed fix for a tricky AOP-related issue.
| export interface EggPrototypeWithClazz extends EggPrototype { | ||
| clazz?: EggProtoImplClass; | ||
| } | ||
|
|
||
| export interface ProtoToCreate { | ||
| name: string; | ||
| proto: EggPrototype; | ||
| } |
There was a problem hiding this comment.
The interfaces EggPrototypeWithClazz and ProtoToCreate appear to be internal implementation details for StandaloneAopContextHook. Since index.ts uses export * from './src/StandaloneAopContextHook', these interfaces are exposed as part of the package's public API. To maintain a clean public API and signal that these are for internal use, it's better to define them without exporting.
| export interface EggPrototypeWithClazz extends EggPrototype { | |
| clazz?: EggProtoImplClass; | |
| } | |
| export interface ProtoToCreate { | |
| name: string; | |
| proto: EggPrototype; | |
| } | |
| interface EggPrototypeWithClazz extends EggPrototype { | |
| clazz?: EggProtoImplClass; | |
| } | |
| interface ProtoToCreate { | |
| name: string; | |
| proto: EggPrototype; | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@standalone/standalone/test/fixtures/singleton-aop-module/main.ts`:
- Around line 15-19: Replace the private-field access this.graph['initialized']
with a public accessor on Graph (e.g., add a method isInitialized(): boolean)
and call that here (if (!this.graph.isInitialized())). Update the Graph class to
expose the new isInitialized() method and ensure existing mutations still set
the private initialized flag internally; keep the rest of the flow (calling
EggContainerFactory.getOrCreateEggObjectFromClazz(QueryNode), casting to
QueryNode, and calling
this.graph.setBoundExecute(queryNode.run.bind(queryNode))) unchanged.
🧹 Nitpick comments (1)
standalone/standalone/test/index.test.ts (1)
238-255: EnsureRunner.destroy()always runs even on assertion failures.A failed assertion will skip the current
runner.destroy()call, potentially leaking load units into later tests. Consider wrapping the test body intry/finally(or anafterEach) to guarantee cleanup.♻️ Suggested fix
const runner = new Runner(fixturePath); await runner.init(); - // First request - Singleton is created, Advice is created in this context - const result1 = await runner.run(); - assert.equal(result1, 'Tool executed: [advised:1] test-input'); - - // Second request - Singleton is reused, Advice must be re-created - // This would fail without StandaloneAopContextHook! - const result2 = await runner.run(); - assert.equal(result2, 'Tool executed: [advised:1] test-input'); - - // Third request - verify it continues to work - const result3 = await runner.run(); - assert.equal(result3, 'Tool executed: [advised:1] test-input'); - - await runner.destroy(); + try { + // First request - Singleton is created, Advice is created in this context + const result1 = await runner.run(); + assert.equal(result1, 'Tool executed: [advised:1] test-input'); + + // Second request - Singleton is reused, Advice must be re-created + // This would fail without StandaloneAopContextHook! + const result2 = await runner.run(); + assert.equal(result2, 'Tool executed: [advised:1] test-input'); + + // Third request - verify it continues to work + const result3 = await runner.run(); + assert.equal(result3, 'Tool executed: [advised:1] test-input'); + } finally { + await runner.destroy(); + }
| if (!this.graph['initialized']) { | ||
| // Get QueryNode only during graph initialization (first request) | ||
| const queryNodeObj = await EggContainerFactory.getOrCreateEggObjectFromClazz(QueryNode); | ||
| const queryNode = queryNodeObj.obj as QueryNode; | ||
| this.graph.setBoundExecute(queryNode.run.bind(queryNode)); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the main.ts file mentioned in the review
find . -path "*/standalone/standalone/test/fixtures/singleton-aop-module/main.ts" -type fRepository: eggjs/tegg
Length of output: 120
🏁 Script executed:
#!/bin/bash
# Find Tool.ts in the same directory structure
find . -path "*/singleton-aop-module/Tool.ts" -type fRepository: eggjs/tegg
Length of output: 120
🏁 Script executed:
#!/bin/bash
# Also search more broadly for Tool.ts that might contain Graph class
find . -name "Tool.ts" -type fRepository: eggjs/tegg
Length of output: 120
🏁 Script executed:
cat -n ./standalone/standalone/test/fixtures/singleton-aop-module/main.tsRepository: eggjs/tegg
Length of output: 1623
🏁 Script executed:
cat -n ./standalone/standalone/test/fixtures/singleton-aop-module/Tool.tsRepository: eggjs/tegg
Length of output: 3071
🏁 Script executed:
find . -name "tsconfig.json" | head -20Repository: eggjs/tegg
Length of output: 862
🏁 Script executed:
# Check the root and standalone directory tsconfig files
cat -n ./tsconfig.json | head -50Repository: eggjs/tegg
Length of output: 1329
🏁 Script executed:
# Check if there's a standalone-specific tsconfig
find ./standalone -name "tsconfig.json" -type fRepository: eggjs/tegg
Length of output: 229
🏁 Script executed:
cat -n ./standalone/standalone/tsconfig.jsonRepository: eggjs/tegg
Length of output: 309
🏁 Script executed:
# Let me also check if the code actually compiles or if there are errors
# Check the test setup to understand how this fixture is used
find ./standalone/standalone/test -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "singleton-aop-module"Repository: eggjs/tegg
Length of output: 96
🏁 Script executed:
cat -n ./standalone/standalone/test/index.test.tsRepository: eggjs/tegg
Length of output: 16123
🏁 Script executed:
# Check if there are any TypeScript compilation errors by looking at the test output
# First, let's check if the fixture has its own tsconfig
find ./standalone/standalone/test/fixtures/singleton-aop-module -name "tsconfig.json"Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# Let's verify the actual behavior of TypeScript with bracket notation on private fields
# by checking if there are any build/compilation logs or errors
ls -la ./standalone/standalone/test/fixtures/singleton-aop-module/Repository: eggjs/tegg
Length of output: 346
🏁 Script executed:
# Check if main.ts is compiled or used as-is
# Look for any .d.ts or .js files
find ./standalone/standalone/test/fixtures/singleton-aop-module -type fRepository: eggjs/tegg
Length of output: 259
🏁 Script executed:
# Check the package.json to understand how the test fixture is set up
cat -n ./standalone/standalone/test/fixtures/singleton-aop-module/package.jsonRepository: eggjs/tegg
Length of output: 187
🏁 Script executed:
# Let's verify whether TypeScript actually rejects bracket notation access to private fields
# by checking TypeScript documentation or looking at similar patterns in the codebase
rg -A 5 -B 5 "\['.*'\]" ./standalone/standalone/test/fixtures/singleton-aop-module/main.tsRepository: eggjs/tegg
Length of output: 581
🏁 Script executed:
# Search for other usages of private field bracket notation in the codebase
rg "\['[a-zA-Z_]" --type ts | head -30Repository: eggjs/tegg
Length of output: 3203
🏁 Script executed:
# Let's verify whether the code compiles without errors
# Check if there's a build script or test output
cd ./standalone/standalone && npm test 2>&1 | grep -A 10 "singleton-aop-module" || echo "Test output not available"Repository: eggjs/tegg
Length of output: 79
🏁 Script executed:
# Let me search for TypeScript documentation or comments about bracket notation and private fields
rg "bracket|private.*\[" --type ts -B 2 -A 2 | head -40Repository: eggjs/tegg
Length of output: 2881
🏁 Script executed:
# Search the codebase for any documentation about private field access patterns
rg -A 3 "private.*=.*false" ./standalone/standalone/test/fixtures/singleton-aop-module/Tool.tsRepository: eggjs/tegg
Length of output: 213
🏁 Script executed:
# Create a simple test to verify TypeScript behavior
cat > /tmp/test-private.ts << 'EOF'
class TestClass {
private value: boolean = false;
}
const obj = new TestClass();
const result = obj['value']; // Does TypeScript reject this?
EOF
# Check what the TypeScript compiler would say
cat /tmp/test-private.tsRepository: eggjs/tegg
Length of output: 198
🏁 Script executed:
# Let's verify if TypeScript actually allows bracket notation access to private fields
# by checking the TypeScript compiler documentation or examining similar patterns
# First, let's check how the test fixture is being imported
rg "singleton-aop-module" ./standalone/standalone/test/index.test.ts -B 2 -A 2Repository: eggjs/tegg
Length of output: 293
🏁 Script executed:
# Check if there's any transpilation or compilation happening for test fixtures
find ./standalone/standalone/test -name "*.ts" -exec grep -l "singleton-aop-module" {} \;Repository: eggjs/tegg
Length of output: 96
🏁 Script executed:
# Let's check the actual TypeScript behavior by looking at whether bracket notation
# is allowed in strict mode. Check if there are any other examples of bracket notation
# access to private fields in the codebase
rg "private.*=" ./standalone/standalone/test/fixtures/singleton-aop-module/ -A 5 | grep -E "\['|\.get\(|getter"Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# The critical check: see if Graph class has any public getter or if initialized is accessed differently elsewhere
rg "initialized" ./standalone/standalone/test/fixtures/singleton-aop-module/Repository: eggjs/tegg
Length of output: 450
🏁 Script executed:
# Let's check if there's a tsconfig that extends from the root and test settings
find ./standalone/standalone/test/fixtures -name "tsconfig.json"Repository: eggjs/tegg
Length of output: 191
🏁 Script executed:
# Check how tests are run - look at package.json or jest config
cat ./standalone/standalone/package.json | head -50Repository: eggjs/tegg
Length of output: 1434
🏁 Script executed:
# Search for test configuration
ls -la ./standalone/standalone/ | grep -E "jest|mocha|vitest"Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# Check the .mocharc or test configuration
find ./standalone/standalone -name ".mocharc*" -o -name "mocha.opts"Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# Check if test fixtures are type-checked during compilation
cat ./standalone/standalone/tsconfig.jsonRepository: eggjs/tegg
Length of output: 225
🏁 Script executed:
# Let's verify the behavior of TypeScript with bracket notation
# by checking if there's any documentation or comments in the code
rg "noCheck|skipLib|@ts-expect-error|@ts-ignore" ./standalone/standalone/test/fixtures/singleton-aop-module/Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# Let me check if the compilation actually includes test fixtures
cat ./standalone/standalone/tsconfig.pub.json 2>/dev/null || echo "File not found"Repository: eggjs/tegg
Length of output: 225
🏁 Script executed:
# The critical question: are test fixtures compiled by TypeScript?
ls -la ./standalone/standalone/dist/test/ 2>/dev/null || echo "Test files not in dist"Repository: eggjs/tegg
Length of output: 76
🏁 Script executed:
# Key finding: test files are excluded from TypeScript compilation
# Let's verify that the code actually works at runtime despite accessing private field
# Check if there's any way test files are type-checked separately
find ./standalone/standalone -name ".mocharc.yml" -o -name ".mocharc.json" -o -name ".mocharc.cjs"Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# Let's see the actual mocha test configuration
cat ./standalone/standalone/.mocharc.yml 2>/dev/null || cat ./standalone/standalone/.mocharc.json 2>/dev/null || find ./standalone/standalone -maxdepth 2 -name ".mocharc*" -exec cat {} \;Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# Check the root-level mocha config that might apply to all tests
cat ./.mocharc.yml 2>/dev/null || cat ./.mocharc.json 2>/dev/null || echo "No root mocha config found"Repository: eggjs/tegg
Length of output: 223
🏁 Script executed:
# Critical check: Look at the actual compile settings
# Since test files are excluded from tsconfig, they're never compiled by tsc
# But they might be type-checked by mocha or another tool
grep -r "ts-node\|ts-mocha\|fork-ts-checker" ./standalone/standalone/ 2>/dev/null || echo "No TypeScript runtime type-checking found"Repository: eggjs/tegg
Length of output: 116
🏁 Script executed:
# Now let's verify the critical fact: Does TypeScript actually reject bracket notation on private fields?
# ts-node uses tsconfig.json with ts-node settings. Let's check if there are any type errors
# by looking at the actual test execution or TypeScript settings
cat ./tsconfig.json | grep -A 20 "ts-node"Repository: eggjs/tegg
Length of output: 92
🏁 Script executed:
# Let me check whether bracket notation access to private fields is actually allowed or rejected
# by looking for any `@ts-ignore` or similar suppressions in the file
cat -n ./standalone/standalone/test/fixtures/singleton-aop-module/main.ts | grep -i "ts-"Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# Create a definitive test: check if TypeScript with strict mode allows bracket notation on private fields
# by examining what the actual TypeScript compiler does
python3 << 'EOF'
# According to TypeScript documentation:
# - Bracket notation does NOT bypass access modifiers
# - obj['privateField'] is rejected just like obj.privateField
# - This is a compile-time check in TypeScript strict mode
print("TypeScript behavior with bracket notation on private fields:")
print("In strict mode, TypeScript rejects BOTH:")
print(" 1. obj.privateField (dot notation)")
print(" 2. obj['privateField'] (bracket notation)")
print("")
print("Both use the same type checking, bracket notation does NOT bypass access control.")
EOFRepository: eggjs/tegg
Length of output: 319
🏁 Script executed:
# Check if there's any CI configuration that might show whether tests pass or fail
find . -maxdepth 3 -name ".github" -type dRepository: eggjs/tegg
Length of output: 63
🏁 Script executed:
# Let me look at git history or any pull request information about this fixture
git log --oneline ./standalone/standalone/test/fixtures/singleton-aop-module/main.ts 2>/dev/null | head -5Repository: eggjs/tegg
Length of output: 130
🏁 Script executed:
# Check if the Graph class provides any alternative accessor method
grep -A 20 "class Graph" ./standalone/standalone/test/fixtures/singleton-aop-module/Tool.tsRepository: eggjs/tegg
Length of output: 593
🏁 Script executed:
# Let me check if there's any CI workflow that would validate this
find ./.github/workflows -name "*.yml" -o -name "*.yaml" | head -5Repository: eggjs/tegg
Length of output: 156
🏁 Script executed:
# Check if there are any recent issues or discussions about this
grep -r "initialized" ./.github/workflows/ 2>/dev/null | head -5Repository: eggjs/tegg
Length of output: 36
🏁 Script executed:
# Most importantly, let's verify whether TypeScript actually rejects this by checking
# if there's a way this fixture could be loaded/imported without type checking
find ./standalone/standalone/test -name "*.ts" -exec grep -l "require\|import.*main" {} \;Repository: eggjs/tegg
Length of output: 96
🏁 Script executed:
# Let's check what the test actually does - does it import and execute the fixture
cat -n ./standalone/standalone/test/index.test.ts | sed -n '220,256p'Repository: eggjs/tegg
Length of output: 2105
🏁 Script executed:
# Let me verify whether ts-node actually type-checks these files or just transpiles them
# by checking if there are any type errors reported when running the tests
grep -A 10 "Runner\|main(" ./standalone/standalone/test/index.test.ts | head -20Repository: eggjs/tegg
Length of output: 791
🏁 Script executed:
# Check the actual import/usage of the singleton-aop-module fixture
rg "singleton-aop-module" ./standalone/standalone/test/index.test.ts -B 5 -A 10Repository: eggjs/tegg
Length of output: 923
🏁 Script executed:
# Let me understand how the main() function loads the fixture
# by checking the Runner and main() implementation
find ./standalone/standalone/src -name "*.ts" | xargs grep -l "class Runner\|function main"Repository: eggjs/tegg
Length of output: 127
🏁 Script executed:
# Check if fixture files are statically type-checked or dynamically loaded
cat -n ./standalone/standalone/src/index.ts 2>/dev/null || find ./standalone/standalone/src -name "index.ts" | head -1 | xargs cat -n | head -50Repository: eggjs/tegg
Length of output: 36
Accessing a private Graph field through bracket notation violates TypeScript's access control.
this.graph['initialized'] accesses a private field and will be rejected by TypeScript even in bracket notation—both dot and bracket access are type-checked for private modifiers in strict mode. Expose a public accessor method on Graph and use that instead.
Proposed fix in this file
- if (!this.graph['initialized']) {
+ if (!this.graph.isInitialized()) {
// Get QueryNode only during graph initialization (first request)
const queryNodeObj = await EggContainerFactory.getOrCreateEggObjectFromClazz(QueryNode);
const queryNode = queryNodeObj.obj as QueryNode;
this.graph.setBoundExecute(queryNode.run.bind(queryNode));
}Corresponding update in Tool.ts
export class Graph {
private boundExecute: ((input: string) => Promise<string>) | null = null;
private initialized = false;
+
+ isInitialized(): boolean {
+ return this.initialized;
+ }🤖 Prompt for AI Agents
In `@standalone/standalone/test/fixtures/singleton-aop-module/main.ts` around
lines 15 - 19, Replace the private-field access this.graph['initialized'] with a
public accessor on Graph (e.g., add a method isInitialized(): boolean) and call
that here (if (!this.graph.isInitialized())). Update the Graph class to expose
the new isInitialized() method and ensure existing mutations still set the
private initialized flag internally; keep the rest of the flow (calling
EggContainerFactory.getOrCreateEggObjectFromClazz(QueryNode), casting to
QueryNode, and calling
this.graph.setBoundExecute(queryNode.run.bind(queryNode))) unchanged.
| * Pre-creates ContextProto advice objects when a new context is initialized. | ||
| * This ensures that advice objects are available when AOP methods are called. | ||
| */ | ||
| export class StandaloneAopContextHook implements LifecycleHook<EggContextLifecycleContext, EggContext> { |
There was a problem hiding this comment.
這個放在 aop runtime 裏是不是更加合適?
Checklist
npm testpassesAffected core subsystem(s)
Description of change
Summary by CodeRabbit