Skip to content

fix (run-time): Promise.all Parsing properly for Dynamic Arrays#166

Merged
zhubzy merged 7 commits intomainfrom
fix/promise-pushes
Dec 6, 2025
Merged

fix (run-time): Promise.all Parsing properly for Dynamic Arrays#166
zhubzy merged 7 commits intomainfrom
fix/promise-pushes

Conversation

@zhubzy
Copy link
Copy Markdown
Contributor

@zhubzy zhubzy commented Dec 5, 2025

Summary

Enhanced Promise.all Parsing for Dynamic Arrays

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Other (please describe):

Checklist

  • My code follows the code style of this project
  • I have added appropriate tests for my changes
  • I have run pnpm check and all tests pass
  • I have tested my changes locally
  • I have linked relevant issues
  • I have added screenshots for UI changes (if applicable)

Screenshots (if applicable)

Additional Context

Copilot AI review requested due to automatic review settings December 5, 2025 23:57
@bubblelab-pearl
Copy link
Copy Markdown
Contributor

Suggested PR title from Pearl

Title: feat(runtime): enhance Promise.all parsing and enforce method call rule

Body:
This PR introduces significant enhancements to the BubbleFlow runtime, improving workflow graph extraction for parallel execution and enforcing a clearer method structure.

Key Changes:

  1. Enhanced Promise.all Parsing for Dynamic Arrays:

    • Problem: Previously, the BubbleParser could only accurately extract parallel execution steps from Promise.all calls when the array of promises was defined as an inline array literal. It failed to identify steps when the array was constructed dynamically (e.g., by pushing promises into a variable within a loop).
    • Solution: The BubbleParser's detectPromiseAll and buildParallelExecutionNode methods have been updated to support Promise.all calls that reference an identifier (variable name) holding the array of promises. A new private helper method, findArrayPushCalls, was added to trace all .push() operations on such array variables.
    • Impact: This enables the workflow graph extraction (extractStepGraph utility) to correctly visualize parallel steps even when the array of promises is built dynamically, providing a more accurate representation of complex flows.
    • Validation: A new comprehensive test case has been added in apps/bubble-studio/src/utils/workflowToSteps.test.ts to validate this improved parsing with a real-world example.
  2. New Lint Rule: noMethodCallingMethodRule:

    • Problem: Without explicit rules, BubbleFlow methods could call other this. methods, leading to nested logic that might obscure the direct flow of steps intended for visualization and make the workflow harder to reason about.
    • Solution: A new lint rule, noMethodCallingMethodRule, has been introduced in packages/bubble-runtime/src/validation/lint-rules.ts. This rule prevents any method within a BubbleFlow class (excluding the main handle method) from calling other this. methods.
    • Rationale: This enforces a flatter, more explicit workflow structure. It ensures that all significant method calls, which typically represent distinct steps in the workflow, are directly orchestrated within the handle method. This improves the clarity and interpretability of the workflow graph and promotes a more predictable execution flow.
    • Validation: A new test case in packages/bubble-runtime/src/validation/index.test.ts verifies the correct enforcement of this rule.
  3. API Simplification:

    • The validateAndExtract function call in apps/bubblelab-api/src/routes/bubble-flows.ts has been simplified by removing an unnecessary strictMode boolean parameter. This parameter is now either implicitly handled or no longer required due to the updated validation logic and the introduction of the new lint rule.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 5, 2025

Deploying bubble-studio with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2510aa2
Status: ✅  Deploy successful!
Preview URL: https://3e9935e2.bubble-studio.pages.dev
Branch Preview URL: https://fix-promise-pushes.bubble-studio.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 5, 2025

Deploying bubblelab-documentation with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2510aa2
Status: ✅  Deploy successful!
Preview URL: https://ed93782d.bubblelab-documentation.pages.dev
Branch Preview URL: https://fix-promise-pushes.bubblelab-documentation.pages.dev

View logs

@zhubzy zhubzy changed the title fix: parsing for promise pushes fix: Promise.all Parsing for Dynamic Arrays Dec 5, 2025
@zhubzy zhubzy changed the title fix: Promise.all Parsing for Dynamic Arrays fix (run-time): Promise.all Parsing properly for Dynamic Arrays Dec 5, 2025
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses two main concerns: (1) enhancing the BubbleParser to handle Promise.all() with dynamically built arrays using .push() calls, and (2) adding a new lint rule to prevent methods from calling other methods within BubbleFlow classes.

Key Changes:

  • Extended Promise.all() parsing to support array variables built with .push() calls, not just array literals
  • Added noMethodCallingMethodRule lint rule requiring that only the handle() method calls other class methods
  • Changed default validation behavior in the API route to require lint errors by default

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/bubble-runtime/src/extraction/BubbleParser.ts Added findArrayPushCalls() method to trace array.push() calls and updated detectPromiseAll() and buildParallelExecutionNode() to handle both ArrayExpression and Identifier types for Promise.all() arguments
packages/bubble-runtime/src/validation/lint-rules.ts Added noMethodCallingMethodRule to enforce that only the handle() method can call other methods, preventing inter-method calls
packages/bubble-runtime/src/validation/index.test.ts Added test case for the new lint rule validation
apps/bubblelab-api/src/routes/bubble-flows.ts Removed the false parameter from validateAndExtract() call, changing behavior to require lint errors by default
apps/bubble-studio/src/utils/workflowToSteps.test.ts Added integration test with example code using Promise.all() with array.push() pattern to verify workflow parsing

Comment thread apps/bubble-studio/src/utils/workflowToSteps.test.ts Outdated
expect(response.ok).toBe(true);

const validationResult: ValidateBubbleFlowResponse = await response.json();
console.log(JSON.stringify(validationResult.workflow, null, 2));
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

[nitpick] The test uses console.log(JSON.stringify(validationResult.workflow, null, 2)) on line 40, which will output to console during test execution. This appears to be debugging code that should be removed.

Suggested change
console.log(JSON.stringify(validationResult.workflow, null, 2));

Copilot uses AI. Check for mistakes.
Comment thread apps/bubble-studio/src/utils/workflowToSteps.test.ts Outdated
Comment on lines +147 to +176
it('should fail validation when a method calls another method', async () => {
const code = `
import { BubbleFlow, AIAgentBubble } from '@bubblelab/bubble-core';

export class TestFlow extends BubbleFlow<'webhook/http'> {
private async helperMethod(): Promise<string> {
return 'test';
}

private async gatherContext(): Promise<string> {
// This should fail - calling another method from a method
const result = await this.helperMethod();
return result;
}

async handle(payload: any): Promise<any> {
const context = await this.gatherContext();
return { context };
}
}
`;
const result = await validateBubbleFlow(code);
expect(result.valid).toBe(false);
expect(result.errors).toBeDefined();
expect(
result.errors?.some((error) =>
error.includes('cannot be called from another method')
)
).toBe(true);
});
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The test case for the noMethodCallingMethodRule lacks comprehensive coverage. It only tests the positive case (a method calling another method should fail). Consider adding test cases for:

  1. The negative case: verify that methods NOT calling other methods pass validation
  2. Verify that the handle method CAN call other methods without triggering errors
  3. Test edge cases like property access on this that aren't method calls (e.g., this.someProperty)

Copilot uses AI. Check for mistakes.
Comment on lines +3370 to +3374
for (const key in node) {
const child = (node as any)[key];
if (Array.isArray(child)) child.forEach(walk);
else if (child?.type) walk(child);
}
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The tree walking implementation in findArrayPushCalls uses for (const key in node) which iterates over all enumerable properties, potentially including inherited properties from the prototype chain. This could lead to unexpected behavior or performance issues. Consider using a more robust AST traversal approach, such as checking for specific AST node properties (like body, expression, arguments, etc.) or using a library-provided traversal utility. Also consider using Object.hasOwnProperty() or Object.keys() to avoid prototype properties.

Copilot uses AI. Check for mistakes.
Comment on lines +808 to +851
/**
* Lint rule that prevents methods from calling other methods
* Methods should only be called from the handle method, not from other methods
*/
export const noMethodCallingMethodRule: LintRule = {
name: 'no-method-calling-method',
validate(context: LintRuleContext): LintError[] {
const errors: LintError[] = [];

if (!context.bubbleFlowClass) {
return errors; // No BubbleFlow class found, skip this rule
}

// Find all methods in the BubbleFlow class
const methods: ts.MethodDeclaration[] = [];
if (context.bubbleFlowClass.members) {
for (const member of context.bubbleFlowClass.members) {
if (ts.isMethodDeclaration(member)) {
// Skip the handle method - it's allowed to call other methods
const methodName = member.name;
if (ts.isIdentifier(methodName) && methodName.text === 'handle') {
continue;
}
methods.push(member);
}
}
}

// For each method, check if it calls other methods
for (const method of methods) {
if (!method.body || !ts.isBlock(method.body)) {
continue;
}

const methodCallErrors = findMethodCallsInNode(
method.body,
context.sourceFile
);
errors.push(...methodCallErrors);
}

return errors;
},
};
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

[nitpick] The PR title is "fix: parsing for promise pushes" but this change adds an unrelated lint rule for preventing methods from calling other methods. This new lint rule should either be in a separate PR or the PR title should be updated to reflect all changes being made (e.g., "fix: parsing for promise pushes and add no-method-calling-method lint rule").

Copilot uses AI. Check for mistakes.

// Create a new BubbleFlowValidationTool instance
const result = await validateAndExtract(code, bubbleFactory, false);
const result = await validateAndExtract(code, bubbleFactory);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The requireLintErrors parameter default value has changed from false to true (defaulting to true when no third parameter is provided). Ensure this behavior change is intentional and that any code relying on the previous default behavior is updated. This could cause previously passing validations to fail if they had lint errors.

Suggested change
const result = await validateAndExtract(code, bubbleFactory);
const result = await validateAndExtract(code, bubbleFactory, false);

Copilot uses AI. Check for mistakes.
this.findVariableIdByName(arrayVarName, pushLine, scopeManager) ===
varId
) {
pushedArgs.push(node.arguments[0] as TSESTree.Expression);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

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

The findArrayPushCalls function only captures the first argument to .push() calls (line 3367). If a user calls array.push(item1, item2) with multiple arguments, only the first argument will be captured. While .push() can accept multiple arguments, this implementation will silently ignore subsequent arguments. Consider either documenting this limitation or handling multiple arguments.

Suggested change
pushedArgs.push(node.arguments[0] as TSESTree.Expression);
node.arguments.forEach(arg => {
pushedArgs.push(arg as TSESTree.Expression);
});

Copilot uses AI. Check for mistakes.
@zhubzy zhubzy merged commit 3eebe61 into main Dec 6, 2025
3 checks passed
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.

3 participants