Skip to content

fix: only marshal promise resolution when created on the runtime loop#396

Open
adrian-niculescu wants to merge 1 commit into
NativeScript:mainfrom
adrian-niculescu:fix/promise-proxy-background-origin
Open

fix: only marshal promise resolution when created on the runtime loop#396
adrian-niculescu wants to merge 1 commit into
NativeScript:mainfrom
adrian-niculescu:fix/promise-proxy-background-origin

Conversation

@adrian-niculescu

@adrian-niculescu adrian-niculescu commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

A promise constructed while native code runs JS on a background thread never resolves. The proxy captures the current run loop at construction, and when the promise later resolves on a different thread it marshals the resolution back to that captured run loop. For a promise created on a background thread, that run loop is dormant once the call returns, so a resolution arriving on the runtime loop (for example from a setTimeout) is scheduled onto a loop nobody pumps and never runs.

This gates the marshaling on origin: a resolution is only marshaled back when the promise was created on the runtime loop, which is always being pumped. A promise created on any other thread resolves inline on whichever thread settles it. The same gate is applied to the get trap on the returned promise. This matches the approach discussed in the issue.

Test: TestRunner/app/tests/Promises.js gets a case that constructs a promise on a background dispatch queue and resolves it on the runtime loop. It times out before the change and passes after; the rest of the suite stays green.

Fixes #330

Summary by CodeRabbit

  • Bug Fixes
    • Improved Promise settlement and callback timing when promises are created on background threads and resolved on the main runtime loop.
    • Enhanced reliability so .then handlers run immediately when appropriate, or dispatch correctly when crossing the thread boundary.
  • Tests
    • Added coverage for Promise resolution from a background queue and completion on the main NSOperationQueue.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 451295d8-0fe0-4952-b933-8a1768213d09

📥 Commits

Reviewing files that changed from the base of the PR and between ebe32ac and 79a74fc.

📒 Files selected for processing (2)
  • NativeScript/runtime/PromiseProxy.cpp
  • TestRunner/app/tests/Promises.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • TestRunner/app/tests/Promises.js

📝 Walkthrough

Walkthrough

Promise proxy installation now passes a runtime-loop callback into the embedded bootstrap, and promise resolve/reject/.then scheduling now checks the runtime loop before marshaling to another runloop. A new test covers background creation with main-queue settlement.

Changes

Promise proxy runtime-loop scheduling

Layer / File(s) Summary
Runtime-loop callback install
NativeScript/runtime/PromiseProxy.cpp
The embedded bootstrap returns an installer function, and C++ creates and passes an isRuntimeRunloop V8 callback into that installer after asserting the script result is a function.
Promise callback scheduling
NativeScript/runtime/PromiseProxy.cpp, TestRunner/app/tests/Promises.js
resolve, reject, and .then execution use the runtime-loop check to choose synchronous delivery or CFRunLoopPerformBlock, and a new test resolves a promise from a background queue through NSOperationQueue.mainQueue.

Sequence Diagram(s)

sequenceDiagram
  participant PromiseProxy as "PromiseProxy.cpp"
  participant RuntimeLoop as "runtime->RuntimeLoop()"
  participant PerformBlock as "CFRunLoopPerformBlock"
  participant WakeUp as "CFRunLoopWakeUp"

  PromiseProxy->>RuntimeLoop: compare current CFRunLoop
  alt current thread matches runtime loop
    PromiseProxy->>PromiseProxy: invoke resolve/reject/.then synchronously
  else current thread differs
    PromiseProxy->>PerformBlock: queue callback on the captured runloop
    PromiseProxy->>WakeUp: wake the target runloop
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I hopped through loops of main-thread light,
Where promises now wake just right.
A background burrow met the day,
And settled values found their way.
🐇 Thump-thump—done!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main behavior change to Promise marshalling on the runtime loop.
Linked Issues check ✅ Passed The PromiseProxy change and new background-thread test address #330 by avoiding marshaling off-runtime-loop promises back to a dormant loop.
Out of Scope Changes check ✅ Passed The diff appears limited to the Promise resolution fix and its test, with no unrelated scope introduced.

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.

@adrian-niculescu adrian-niculescu marked this pull request as ready for review June 25, 2026 21:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
TestRunner/app/tests/Promises.js (1)

142-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fail explicitly on unexpected rejection.

If this promise rejects, the test currently hangs until timeout, which obscures the failure. Add a catch that fails and completes the async spec.

Proposed test hardening
             new Promise(resolve => {
                 NSOperationQueue.mainQueue.addOperationWithBlock(() => resolve("settled"));
             }).then(value => {
                 expect(value).toBe("settled");
                 done();
+            }).catch(error => {
+                expect(true).toBe(false, `Promise rejected unexpectedly: ${error}`);
+                done();
             });
🤖 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 `@TestRunner/app/tests/Promises.js` around lines 142 - 147, The Promise test in
the async spec currently only handles the success path, so unexpected rejection
can hang the test until timeout. Update the Promise chain in the `Promises.js`
test case to add a `catch` alongside the existing `then` so any rejection
explicitly fails the spec and calls `done()`. Keep the change focused on the
affected promise block near the
`NSOperationQueue.mainQueue.addOperationWithBlock` usage and the
`expect(value).toBe("settled")` assertion.
🤖 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.

Nitpick comments:
In `@TestRunner/app/tests/Promises.js`:
- Around line 142-147: The Promise test in the async spec currently only handles
the success path, so unexpected rejection can hang the test until timeout.
Update the Promise chain in the `Promises.js` test case to add a `catch`
alongside the existing `then` so any rejection explicitly fails the spec and
calls `done()`. Keep the change focused on the affected promise block near the
`NSOperationQueue.mainQueue.addOperationWithBlock` usage and the
`expect(value).toBe("settled")` assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 95e3cfae-e63f-4054-b4fc-cd4897841294

📥 Commits

Reviewing files that changed from the base of the PR and between 54bcc22 and ebe32ac.

📒 Files selected for processing (2)
  • NativeScript/runtime/PromiseProxy.cpp
  • TestRunner/app/tests/Promises.js

The promise proxy captures the run loop at construction and, when a
promise resolves on a different thread, marshals the resolution back to
that captured run loop. When a promise is constructed while native code
runs JS on a background thread, the captured run loop belongs to that
background thread, which is dormant once the call returns. A resolution
that arrives later on the runtime loop (for example from a setTimeout)
is scheduled onto the dormant run loop and never runs, so the promise
hangs forever.

Only marshal the resolution when the promise was created on the runtime
loop, which is always being pumped. A promise created on any other
thread resolves inline on whichever thread settles it. The same gate is
applied to the get trap on the returned promise.

Fixes NativeScript#330
@adrian-niculescu adrian-niculescu force-pushed the fix/promise-proxy-background-origin branch from ebe32ac to 79a74fc Compare June 25, 2026 21:38
@adrian-niculescu adrian-niculescu marked this pull request as draft June 25, 2026 21:38
@adrian-niculescu adrian-niculescu marked this pull request as ready for review June 25, 2026 21:41
@adrian-niculescu

Copy link
Copy Markdown
Contributor Author

🧹 Nitpick comments (1)

TestRunner/app/tests/Promises.js (1)> 142-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fail explicitly on unexpected rejection.
If this promise rejects, the test currently hangs until timeout, which obscures the failure. Add a catch that fails and completes the async spec.

Proposed test hardening

             new Promise(resolve => {
                 NSOperationQueue.mainQueue.addOperationWithBlock(() => resolve("settled"));
             }).then(value => {
                 expect(value).toBe("settled");
                 done();
+            }).catch(error => {
+                expect(true).toBe(false, `Promise rejected unexpectedly: ${error}`);
+                done();
             });

🤖 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 `@TestRunner/app/tests/Promises.js` around lines 142 - 147, The Promise test in
the async spec currently only handles the success path, so unexpected rejection
can hang the test until timeout. Update the Promise chain in the `Promises.js`
test case to add a `catch` alongside the existing `then` so any rejection
explicitly fails the spec and calls `done()`. Keep the change focused on the
affected promise block near the
`NSOperationQueue.mainQueue.addOperationWithBlock` usage and the
`expect(value).toBe("settled")` assertion.

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

Nitpick comments:
In `@TestRunner/app/tests/Promises.js`:
- Around line 142-147: The Promise test in the async spec currently only handles
the success path, so unexpected rejection can hang the test until timeout.
Update the Promise chain in the `Promises.js` test case to add a `catch`
alongside the existing `then` so any rejection explicitly fails the spec and
calls `done()`. Keep the change focused on the affected promise block near the
`NSOperationQueue.mainQueue.addOperationWithBlock` usage and the
`expect(value).toBe("settled")` assertion.

ℹ️ Review info

@coderabbitai, fixed now.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@adrian-niculescu Thanks for the update — marking this as addressed.

(ᵔᴥᵔ)


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

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.

Promise never resolves when resolve() is called inside setTimeout from a background thread

1 participant