-
-
Notifications
You must be signed in to change notification settings - Fork 374
cache iframe loaded state to allow subsequent message sending #2909
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Summary of ChangesHello @imolorhe, 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 issue where messages sent to an iframe were not reliably delivered after its initial load. By implementing a mechanism to cache the iframe's loaded state using a promise, the system now ensures that all subsequent message sending operations correctly wait for the iframe to be fully ready, thereby guaranteeing reliable communication and improving the robustness of the application. Highlights
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
|
Reviewer's guide (collapsed on small PRs)Reviewer's GuideThis PR introduces a cached iframe-loaded promise to track when the iframe is ready and refactors the send method to await this promise before posting messages, ensuring that all messages are sent only after the iframe has fully loaded. Sequence diagram for sending messages to iframe after loadsequenceDiagram
participant Client as EvaluatorFrameClient
participant Iframe
Client->>Iframe: Add 'load' event listener (on creation)
Client->>Client: Create iframeLoadedPromise
Client->>Iframe: send(type, payload)
Client->>Client: await iframeLoadedPromise
Iframe-->>Client: 'load' event resolves promise
Client->>Iframe: postMessage({ type, payload }, sandboxUrl)
Class diagram for updated EvaluatorFrameClientclassDiagram
class ScriptEvaluatorClient {
}
class EvaluatorFrameClient {
- iframeLoadedPromise: Promise<void>
+ constructor(sandboxUrl: string)
+ send(type: string, payload: any): Promise<void>
+ onError(handler: (err: any) => void): void
}
ScriptEvaluatorClient <|-- EvaluatorFrameClient
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughRefactors the pre-request EvaluatorFrameClient to await a single iframeLoadedPromise that resolves on the iframe load event before sending messages. The send method is now async, returns Promise, and posts messages only after the iframe has loaded. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant PR as Pre-request Script
participant EFC as EvaluatorFrameClient
participant IF as Iframe (Evaluator)
Note over EFC,IF: Initialization
EFC->>IF: Create iframe and attach 'load' listener
IF-->>EFC: 'load' event resolves iframeLoadedPromise
Note over PR,EFC: Sending a message
PR->>EFC: send(message)
activate EFC
EFC->>EFC: await iframeLoadedPromise
EFC->>IF: postMessage(message)
deactivate EFC
Note over PR,IF: Response handling (unchanged)
IF-->>PR: message response via postMessage
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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 aims to fix an issue where messages could not be sent to the iframe after the initial load. The approach of using a promise (iframeLoadedPromise) to cache the loaded state is correct. However, the current implementation has two main issues. First, there is a critical race condition in the initialization of iframeLoadedPromise that could cause it to never resolve. Second, changing the send method to be asynchronous breaks the contract with the base ScriptEvaluatorClient class. My review includes detailed comments on how to address both of these issues.
| private iframeLoadedPromise = new Promise<void>((resolve) => { | ||
| this.iframe.addEventListener('load', () => { | ||
| resolve(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a potential race condition here. The iframe is created and its src is set within createIframe(), which is called during the initialization of the iframe property. The browser might start loading and fire the load event before this iframeLoadedPromise is initialized and the load event listener is attached. If that happens, the promise will never resolve, causing send to hang indefinitely.
To fix this, you should ensure the load event listener is attached before the iframe.src is set. This typically requires refactoring the initialization logic. For example, you could move the iframe creation and setup into the constructor to control the order of operations:
export class EvaluatorFrameClient extends ScriptEvaluatorClient {
private iframe: HTMLIFrameElement;
private readonly iframeLoadedPromise: Promise<void>;
// ...
constructor(private sandboxUrl: string) {
super();
this.iframe = document.createElement('iframe');
this.iframeLoadedPromise = new Promise<void>(resolve => {
this.iframe.addEventListener('load', () => resolve());
});
this.iframe.style.display = 'none';
const sandboxUrl = new URL(this.sandboxUrl);
sandboxUrl.searchParams.set('sc', window.location.origin);
sandboxUrl.searchParams.set('action', 'evaluator');
this.iframe.src = sandboxUrl.toString();
this.iframe.sandbox.add('allow-scripts');
this.iframe.sandbox.add('allow-same-origin');
this.iframe.referrerPolicy = 'no-referrer';
document.body.appendChild(this.iframe);
// You would need to remove the `createIframe` method and the `iframe` property initializer.
}
// ...
}This ensures the listener is always attached before loading begins.
| async send(type: string, payload: any): Promise<void> { | ||
| await this.iframeLoadedPromise; | ||
| this.iframe.contentWindow?.postMessage({ type, payload }, this.sandboxUrl); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing the return type of send from void to Promise<void> is a good fix for this class, but it makes it inconsistent with the base ScriptEvaluatorClient abstract class, which defines send as returning void. This violates the Liskov substitution principle and can lead to unexpected behavior, especially since the other implementation EvaluatorWorkerClient still returns void.
To fix this properly, you should:
- Update the
sendmethod signature in the abstract classScriptEvaluatorClient(inpackages/altair-core/src/script/types.ts) toabstract send(type: string, payload: any): Promise<void>;. - Update the
sendmethod inEvaluatorWorkerClientto also beasyncand return aPromise<void>.
// in EvaluatorWorkerClient
async send(type: string, payload: any): Promise<void> {
this.worker.postMessage({
type,
payload,
});
}This will ensure all implementations are consistent and callers can reliably await the send operation.
|
Visit the preview URL for this PR (updated for commit bdfe573): https://altair-gql--pr2909-imolorhe-fix-evaluat-ppo341sn.web.app (expires Sat, 18 Oct 2025 01:47:29 GMT) 🔥 via Firebase Hosting GitHub Action 🌎 Sign: 02d6323d75a99e532a38922862e269d63351a6cf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/altair-app/src/app/modules/altair/services/pre-request/evaluator-client.factory.ts (1)
75-75: Remove dead code:removeAllListenersdoesn't exist on HTMLIFrameElement.The
removeAllListenersmethod is not part of the HTMLIFrameElement API. This line has no effect.this.messageListeners.forEach((listener) => { window.removeEventListener('message', listener); }); - this.iframe.removeAllListeners?.(); this.iframe.remove();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
packages/altair-app/src/app/modules/altair/services/pre-request/evaluator-client.factory.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit inference engine (.github/instructions/app-testing.instructions.md)
Follow project code style using ESLint and Prettier
**/*.ts: Use explicit type annotations for function parameters and return types
Prefer interfaces over type aliases for object shapes
Use union types and literal types for better type safety
Leverage generic types for reusable components
Group imports: external libraries first, then internal modules
Use absolute imports from package roots when possible
Prefer named exports over default exports
Use custom error classes that extend Error
Implement proper error boundaries and handling
Log errors with sufficient context for debugging
Use observables (RxJS) for reactive programming patterns where appropriate
Manage subscriptions to avoid memory leaks
Use appropriate RxJS operators for data transformation
Handle errors in observable streams
Use async/await for sequential operations
Handle promise rejections properly
Use Promise.all() for concurrent operations
Implement timeout handling for long-running operations
Dispose of resources properly (subscriptions, event listeners)
Use weak references where appropriate
Avoid creating unnecessary objects in hot paths
Profile memory usage for performance-critical code
Use tree-shaking-friendly imports
Lazy load heavy modules when possible
Monitor bundle size impacts of new dependencies
Use dynamic imports for code splitting
Validate and sanitize all user inputs
Implement proper XSS and injection prevention
Validate API responses before processing
Sanitize sensitive data in logs
Follow secure coding practices
Group related functionality in modules
Keep files focused and not too large
Use consistent naming conventions
Organize imports and exports clearly
Write JSDoc comments for public APIs
Keep documentation up to date with code changes (inline docs)
Use meaningful variable and function names
Handle environment-specific APIs properly
Use TypeScript features appropriate for the configured version
Files:
packages/altair-app/src/app/modules/altair/services/pre-request/evaluator-client.factory.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/instructions/main.instructions.md)
Use TypeScript for implementation across the codebase
Files:
packages/altair-app/src/app/modules/altair/services/pre-request/evaluator-client.factory.ts
packages/altair-app/**/*.{ts,html}
📄 CodeRabbit inference engine (.github/instructions/main.instructions.md)
Implement and modify the main web app using Angular conventions within packages/altair-app
Files:
packages/altair-app/src/app/modules/altair/services/pre-request/evaluator-client.factory.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
- GitHub Check: sites / Deploy Login Redirect / Deploy Login Redirect
- GitHub Check: sites / Deploy Sandbox / Deploy Iframe Sandbox
- GitHub Check: sites / Deploy Web App / Deploy Web App
- GitHub Check: sites / Deploy Docs / Deploy Documentation
- GitHub Check: Sourcery review
- GitHub Check: electron / electron (windows-latest)
- GitHub Check: electron / electron (ubuntu-latest)
- GitHub Check: tests / test (macos-latest)
- GitHub Check: electron / electron (macos-latest)
- GitHub Check: docker / docker
- GitHub Check: tests / test (ubuntu-latest)
- GitHub Check: tests / api-e2e-test
- GitHub Check: tests / e2e-test (macos-latest)
- GitHub Check: tests / e2e-test (ubuntu-latest)
- GitHub Check: tauri / tauri (windows-latest)
- GitHub Check: tauri / tauri (macos-latest, --target x86_64-apple-darwin)
- GitHub Check: tauri / tauri (ubuntu-22.04)
- GitHub Check: tauri / tauri (macos-latest, --target aarch64-apple-darwin)
- GitHub Check: build_and_preview
| private iframeLoadedPromise = new Promise<void>((resolve) => { | ||
| this.iframe.addEventListener('load', () => { | ||
| resolve(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling to prevent indefinite hangs if iframe fails to load.
The cached promise approach correctly fixes the reported bug. However, if the iframe fails to load, iframeLoadedPromise never resolves, causing all send() calls to hang indefinitely.
Consider this diff to add error and timeout handling:
- private iframeLoadedPromise = new Promise<void>((resolve) => {
- this.iframe.addEventListener('load', () => {
- resolve();
- });
- });
+ private iframeLoadedPromise = new Promise<void>((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ reject(new Error('Iframe failed to load within timeout'));
+ }, 30000); // 30 second timeout
+
+ this.iframe.addEventListener('load', () => {
+ clearTimeout(timeoutId);
+ resolve();
+ });
+
+ this.iframe.addEventListener('error', (e) => {
+ clearTimeout(timeoutId);
+ reject(new Error('Iframe failed to load: ' + e.message));
+ });
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private iframeLoadedPromise = new Promise<void>((resolve) => { | |
| this.iframe.addEventListener('load', () => { | |
| resolve(); | |
| }); | |
| }); | |
| private iframeLoadedPromise = new Promise<void>((resolve, reject) => { | |
| // Reject if iframe doesn’t load within 30 seconds | |
| const timeoutId = setTimeout(() => { | |
| reject(new Error('Iframe failed to load within timeout')); | |
| }, 30000); | |
| this.iframe.addEventListener('load', () => { | |
| clearTimeout(timeoutId); | |
| resolve(); | |
| }); | |
| this.iframe.addEventListener('error', (e) => { | |
| clearTimeout(timeoutId); | |
| reject(new Error('Iframe failed to load: ' + (e as ErrorEvent).message)); | |
| }); | |
| }); |
🤖 Prompt for AI Agents
In
packages/altair-app/src/app/modules/altair/services/pre-request/evaluator-client.factory.ts
around lines 19 to 23, the iframeLoadedPromise never rejects if the iframe fails
to load, causing send() calls to hang; change the promise to attach both 'load'
and 'error' listeners, implement a configurable timeout that rejects the promise
if neither event fires within the deadline, and ensure listeners and the timeout
are cleaned up when one of the outcomes occurs so the promise always settles
(resolve on load, reject on error or timeout) and upstream callers can handle
failures.
| async send(type: string, payload: any): Promise<void> { | ||
| await this.iframeLoadedPromise; | ||
| this.iframe.contentWindow?.postMessage({ type, payload }, this.sandboxUrl); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify that the base class signature matches or callers handle the Promise.
The logic correctly ensures messages are sent only after the iframe has loaded, which fixes the hang issue.
The method signature changed from returning void to returning Promise<void>. Please verify:
🏁 Script executed:
#!/bin/bash
# Description: Verify the base class signature and all callers of send() method
# Check base class signature
echo "=== Base class ScriptEvaluatorClient.send signature ==="
ast-grep --pattern $'class ScriptEvaluatorClient {
$$$
send($_, $_) {
$$$
}
$$$
}'
# Find all invocations of send() on EvaluatorFrameClient or ScriptEvaluatorClient
echo -e "\n=== Callers of send() method ==="
rg -nP --type=ts -C3 '\b(evaluator|client)\.send\s*\(' -g '!**/node_modules/**'Length of output: 89
🏁 Script executed:
#!/bin/bash
# Show evaluator-client.factory.ts for context
echo "=== evaluator-client.factory.ts (lines 1-200) ==="
sed -n '1,200p' packages/altair-app/src/app/modules/altair/services/pre-request/evaluator-client.factory.ts
# Find and inspect ScriptEvaluatorClient class
echo "=== ScriptEvaluatorClient files ==="
rg -l --type ts "class ScriptEvaluatorClient" -g '!**/node_modules/**'
echo "=== Inspecting ScriptEvaluatorClient implementation ==="
for f in $(rg -l --type ts "class ScriptEvaluatorClient" -g '!**/node_modules/**'); do
echo "--- File: $f ---"
sed -n '1,200p' "$f"
done
# Identify any EvaluatorClient interface
echo "=== EvaluatorClient interface(s) ==="
rg -n --type ts "interface.*EvaluatorClient" -g '!**/node_modules/**'
# Find all send() invocations across the codebase
echo "=== send() callers ==="
rg -n --type ts "\.send\s*\(" -g '!**/node_modules/**' -C2Length of output: 33664
Align send() signature with the base class
The abstract ScriptEvaluatorClient.send(type: string, payload: TODO) returns void, but EvaluatorFrameClient.send is declared async and returns Promise<void>. Either remove async (e.g. use this.iframeLoadedPromise.then(...)) so it returns void, or update the base abstract method to return Promise<void>.
🤖 Prompt for AI Agents
packages/altair-app/src/app/modules/altair/services/pre-request/evaluator-client.factory.ts
lines 64-67: the concrete send() is async and awaits iframeLoadedPromise but the
abstract ScriptEvaluatorClient.send is declared to return void; update the
abstract method signature to return Promise<void> so it matches async
implementations, then update any other subclasses/implementations to return a
Promise (adjust implementations if they were synchronous) and ensure TypeScript
types/imports compile.
Currently only the initialisation message is sent to the iframe and subsequent calls don't satisfy the
loadevent so never gets sent.Fixes
#2908
Checks
yarn test-buildChanges proposed in this pull request:
Summary by Sourcery
Ensure that messages sent to the sandbox iframe after initialization are delivered by caching its load state and awaiting it in the send method
Bug Fixes:
Enhancements:
Summary by CodeRabbit