-
Notifications
You must be signed in to change notification settings - Fork 46
top-level await #261
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
top-level await #261
Conversation
🦋 Changeset detectedLatest commit: c81afe9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
🐳 Docker Images PublishedDefault (no Python): FROM cloudflare/sandbox:0.0.0-pr-261-9476f46With Python: FROM cloudflare/sandbox:0.0.0-pr-261-9476f46-pythonVersion: Use the |
Add documentation for new top-level await feature in JavaScript contexts: - No IIFE wrapper needed for async code - Last expression automatically returned as result - Seamless support for multiple await expressions Updated: - API reference (interpreter.mdx) with top-level await example - How-to guide (code-execution.mdx) with comprehensive examples Related to cloudflare/sandbox-sdk#261 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Document JavaScript top-level await feature that allows developers to write async code without IIFE wrappers. Include examples of automatic expression return behavior and state persistence across executions. Related PR: cloudflare/sandbox-sdk#261 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
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.
Claude Code Review
The type safety and architecture issues from previous reviews are resolved. Implementation is solid with excellent test coverage.
Critical blocker: Missing changeset file. This adds a new feature (top-level await) but has no changeset. Create .changeset/top-level-await.md:
---
'@cloudflare/sandbox': minor
---
Add top-level await support for JavaScript code execution
JavaScript code can now use await at the top level without wrapping in async IIFE.Use minor (not patch) - this is a new feature, not a bug fix.
Previous feedback status:
- Silent catch block (code-transformer.ts:39) - acceptable as-is. Node executor is a standalone process without logger infrastructure, and the fallback behavior is correct
- Documentation could be enhanced but current comments are adequate
- Test coverage is strong, though adding arrow function/generator tests would be nice
Code quality:
- AST-based approach using acorn is much more robust than regex wrapping
- String slicing with AST positions is safe (parses trimmed string, slices same string)
- Semicolon regex
/;$/is safe (only matches end-of-string) - No security issues identified
- Clean separation: transformer → executor → VM
Verdict: Excellent implementation. Add the changeset file and this is ready to merge.
ghostwriternr
left a 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.
Thanks a lot for this change! Overall, this approach makes sense but I think our solution is still incomplete/incorrect (mainly based on the e2e test change) - I wonder if we should just duplicate the logic Node's REPL uses (repl/await.js) with all the additions/logic they handle but cleanly extracted out as a util like you. This logic is not very frequently updated anyway and seems to be super stable.
packages/sandbox-container/src/runtime/executors/javascript/node_executor.ts
Show resolved
Hide resolved
packages/sandbox-container/src/runtime/executors/shared/code-transformer.ts
Show resolved
Hide resolved
Co-authored-by: Naresh <[email protected]>
Document the new top-level await functionality for JavaScript and TypeScript code execution in the Code Interpreter. This feature allows users to write async code without manually wrapping it in async IIFEs, and automatically returns the last expression's value. Related to cloudflare/sandbox-sdk#261 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
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.
Claude Code Review
RECOMMENDATION: REQUEST CHANGES - BLOCKING ISSUE IDENTIFIED
Critical: Variable Persistence Regression
The IIFE wrapping pattern breaks variable persistence across code executions. Each execution creates a new function scope, so variables declared with const/let/var don't persist between calls.
Evidence: Six E2E tests were modified to use globalThis instead of normal variable declarations (lines 750, 820, 846, 895, 907, 951). This proves the implementation broke existing functionality.
Real-world failure:
// Execution 1
const data = await fetch('https://api.example.com').then(r => r.json());
// Execution 2
console.log(data); // ❌ ReferenceError: data is not definedThis happens because each execution wraps code in (async () => { ... })(), creating isolated function scope. The persistent vm context is still there, but variables are scoped to each IIFE.
Why this matters: LLMs generate idiomatic JavaScript using const/let/var, not globalThis assignments. Multi-step workflows (fetch → process → display) will fail.
Required fixes:
- Fix variable persistence without requiring
globalThis - Add E2E tests verifying persistence across separate
execute()calls - Revert the
globalThistest modifications
Possible solutions: conditional wrapping (only when await present), variable hoisting to global scope, or AsyncFunction constructor instead of IIFE.
Important: Timeout Only Applies to Sync Phase
The timeout option (line 85) only covers vm.runInContext(), not the promise resolution. The IIFE returns instantly, so timeout can't catch infinite async loops:
// This times out (sync)
while(true) {}
// This does NOT timeout (async)
await new Promise(resolve => {
setInterval(() => {}, 1000); // Never resolves
});Recommend adding timeout wrapper for promise resolution or documenting this limitation.
Minor: Parse Error Line Numbers
When acorn parsing fails (line 39), code is wrapped anyway and runtime errors report wrong line numbers (off by 1 due to IIFE wrapper).
What Works Well
- ✅ Type safety:
@types/acornproperly added, no implicitany - ✅ AST-based transformation more robust than regex
- ✅ Comprehensive unit tests (162 lines) cover edge cases
- ✅ Clean code organization following established patterns
- ✅ Proper error handling with try-catch
- ✅ Thenable type guard correctly implemented
Test Coverage Gap
All 4 new E2E tests verify single-execution behavior only. None test variable persistence across multiple executions - the primary use case for code interpreters.
Missing scenarios from reviewer request:
- Variables defined with await persisting across executions
- Promise resolution without await keyword
- Multi-step async workflows
The variable persistence bug is blocking. The implementation needs a different approach that supports both top-level await AND variable persistence without non-idiomatic workarounds.
…sync; update tests
Add documentation for new top-level await capability in JavaScript/TypeScript code execution. Variables declared with const, let, or var now persist across executions within a context, enabling multi-step workflows. Related PR: cloudflare/sandbox-sdk#261
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.
Claude Code Review
The variable hoisting implementation is correct - script-level let declarations in vm.runInContext() do persist across executions as intended. The timeout handling now properly covers both sync and async execution paths.
Critical issue remaining: Anonymous function/class declarations will crash at runtime (code-transformer.ts:136, 154).
To fix before merge:
- Add null checks for
decl.idinprocessFunctionDeclarationandprocessClassDeclaration - Handle edge case of declarations without initializers creating empty body (code-transformer.ts:60)
- Add E2E test for the new persistence feature (currently only transformation is tested)
Nice to have:
- Validate AST nodes have required position properties before slicing (defensive coding)
- Consider using
varinstead ofletfor hoisted variables to avoid TDZ edge cases
Overall: Good implementation of a complex feature with comprehensive unit tests. The architecture is sound once the crash bugs are fixed.
packages/sandbox-container/src/runtime/executors/shared/code-transformer.ts
Show resolved
Hide resolved
packages/sandbox-container/src/runtime/executors/shared/code-transformer.ts
Show resolved
Hide resolved
packages/sandbox-container/src/runtime/executors/shared/code-transformer.ts
Show resolved
Hide resolved
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.
Claude Code Review
The implementation is solid. All critical issues from the previous review have been addressed:
- ✅ Anonymous function/class null checks are present (lines 136, 158)
- ✅ Empty body handling for
let x;works correctly - ✅ vm.Context integration tests added (lines 299-378)
- ✅ Timeout now applies to both sync and async execution
- ✅ Type safety excellent - @types/acorn properly used
The hoisting pattern correctly enables REPL-style variable persistence. AST parsing with acorn is the right choice for this use case.
One minor improvement:
Type the catch clause at code-transformer.ts:307 for consistency:
} catch (error: unknown) {
// Parse error - defer to V8 for better error messages
return `(async () => {\n${trimmed}\n})()`;
}This is non-blocking. The PR is ready to merge.
top level await, i played a lot with just wrapping all code and tbh, i wasn't happy with it, felt hacky
so now we parse code with acord, then we check if last AST node is an ExpressionStatement, if the answer is yes: wrap code with return on last expression, if no: wrap code without return (returns undefined)