Skip to content

fix(google-maps): close races in resolveQueryToLatLng#693

Merged
harlan-zw merged 2 commits intomainfrom
fix/google-maps-resolve-query-races
Apr 9, 2026
Merged

fix(google-maps): close races in resolveQueryToLatLng#693
harlan-zw merged 2 commits intomainfrom
fix/google-maps-resolve-query-races

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

🔗 Linked issue

Related to #689 (split out of the umbrella PR; surfaced by CodeRabbit's review of #692).

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

resolveQueryToLatLng (the publicly exposed map helper) had two pre-existing bugs that could leave it hung or throw, depending on timing.

  1. Watcher race after await load(). The wait pattern installed a non-immediate watcher on mapsApi. When load() populated mapsApi synchronously, the watcher missed the change and the promise hung forever.
  2. Null map.value when constructing PlacesService. The function is publicly exposed and can be called before onLoaded populates map.value. The new mapsApi.value!.places.PlacesService(map.value!) line would throw on the bang.

Extracted a waitForMapsReady helper into useGoogleMapsResource that mirrors importLibrary's correct pattern: short-circuit when both refs are already set, re-check after await load(), then install an { immediate: true } watcher inside a detached effectScope so it's safe to call from any context (component setup, exposed methods, tests). The watcher waits for both mapsApi and map, and rejects if status enters 'error'.

Pure bug fix — no API changes. All existing protected behaviours (skip-if-equal center watcher, setOptions exclusion of zoom/center, InfoWindow group close, overlay data-state) remain intact.

🧪 Tests

Added 6 unit tests for waitForMapsReady in test/unit/google-maps-lifecycle.test.ts:

  • already-ready short-circuit (no load() call)
  • synchronous load() resolution does not hang (the original bug)
  • async ref population resolves normally
  • pre-existing 'error' status rejects synchronously without calling load()
  • transition-to-error during the wait rejects
  • waits for map even when mapsApi is set first

All 63 google-maps-* unit tests pass; lint, typecheck, build clean.

resolveQueryToLatLng could hang forever or throw, depending on timing:

1. The wait pattern after `await load()` used a non-immediate watcher.
   When load() populated mapsApi synchronously the watcher missed the
   change and the promise never resolved.
2. PlacesService construction dereferenced `map.value!`, but the
   function is publicly exposed and could be called before onLoaded
   populated the map ref, throwing on the bang.

Extract a `waitForMapsReady` helper into useGoogleMapsResource that
mirrors importLibrary's correct pattern: short-circuit when both refs
are already set, re-check after `await load()`, then install an
immediate watcher inside a detached effect scope. The watcher waits
for both mapsApi and map and rejects if status enters 'error'.
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-playground Ready Ready Preview, Comment Apr 9, 2026 1:09pm

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 9, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@693

commit: cdf1624

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4656ef36-a536-470f-bb8e-c169e0bd81b8

📥 Commits

Reviewing files that changed from the base of the PR and between cdf1624 and e0ca92a.

📒 Files selected for processing (1)
  • test/unit/google-maps-lifecycle.test.ts

📝 Walkthrough

Walkthrough

The PR extracts inline readiness logic from ScriptGoogleMaps.vue into a new async helper waitForMapsReady in useGoogleMapsResource.ts. Components now await this helper to ensure both the Google Maps API ref and the map ref are populated (or reject on error) instead of using a manual watch-based promise. waitForMapsReady uses effectScope and watch to observe mapsApi, map, and status, calls load() when appropriate, and ensures cleanup. Unit tests were added to cover immediate/synchronous/asynchronous resolution and error cases for the new helper.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: fixing race conditions in the resolveQueryToLatLng function, which aligns perfectly with the core bug fixes described in the PR.
Description check ✅ Passed The description provides comprehensive context about the two bugs fixed, the solution approach, and test coverage, directly relating to the changeset across all three modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/google-maps-resolve-query-races

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/unit/google-maps-lifecycle.test.ts (1)

404-410: Avoid wall-clock timeouts in the pending-state assertions.

The 30ms/100ms setTimeout races can flake on slow CI even when the behavior is correct. A settlement flag or fake timers gives the same coverage without depending on scheduler latency.

♻️ Possible rewrite without real timers
   it('does not hang when load() resolves synchronously', async () => {
@@
-    await expect(
-      Promise.race([
-        waitForMapsReady({ mapsApi, map, status, load }),
-        new Promise((_, reject) => setTimeout(() => reject(new Error('hang')), 100)),
-      ]),
-    ).resolves.toBeUndefined()
+    await expect(waitForMapsReady({ mapsApi, map, status, load })).resolves.toBeUndefined()
@@
   it('waits for map even when mapsApi is set first', async () => {
@@
-    const racedEarly = await Promise.race([
-      promise.then(() => 'resolved'),
-      new Promise(resolve => setTimeout(resolve, 30, 'pending')),
-    ])
-    expect(racedEarly).toBe('pending')
+    let settled = false
+    promise.then(() => {
+      settled = true
+    })
+    await nextTick()
+    expect(settled).toBe(false)

Also applies to: 470-475

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/google-maps-lifecycle.test.ts` around lines 404 - 410, The test
currently races waitForMapsReady(...) against a real setTimeout which causes
flaky CI; update the test to avoid wall-clock timers by either (a) making
waitForMapsReady return a settlement signal you can await (e.g., a promise or
resolved flag) and assert that it resolves without using setTimeout, or (b) use
Jest fake timers (jest.useFakeTimers()/advanceTimersByTime) and replace the new
Promise timeout with a controlled timer advance; target the call site using
waitForMapsReady({ mapsApi, map, status, load }) (also update the similar
pattern at lines ~470-475) so the assertion no longer depends on real scheduler
latency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/unit/google-maps-lifecycle.test.ts`:
- Around line 404-410: The test currently races waitForMapsReady(...) against a
real setTimeout which causes flaky CI; update the test to avoid wall-clock
timers by either (a) making waitForMapsReady return a settlement signal you can
await (e.g., a promise or resolved flag) and assert that it resolves without
using setTimeout, or (b) use Jest fake timers
(jest.useFakeTimers()/advanceTimersByTime) and replace the new Promise timeout
with a controlled timer advance; target the call site using waitForMapsReady({
mapsApi, map, status, load }) (also update the similar pattern at lines
~470-475) so the assertion no longer depends on real scheduler latency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: acec8b6a-0252-4c70-bd64-185e5f2d643f

📥 Commits

Reviewing files that changed from the base of the PR and between b59cd35 and cdf1624.

📒 Files selected for processing (3)
  • packages/script/src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue
  • packages/script/src/runtime/components/GoogleMaps/useGoogleMapsResource.ts
  • test/unit/google-maps-lifecycle.test.ts

CodeRabbit flagged the setTimeout-based race patterns as potentially
flaky on slow CI. Replace with deterministic alternatives:

- "does not hang" test now relies on vitest's per-test timeout as the
  backstop. The fix prevents the hang, so the await resolves
  immediately on the happy path.
- "waits for map" test uses a settled flag plus nextTick flushes
  instead of a wall-clock setTimeout race.
@harlan-zw harlan-zw merged commit 33ebf61 into main Apr 9, 2026
14 of 16 checks passed
@harlan-zw harlan-zw deleted the fix/google-maps-resolve-query-races branch April 9, 2026 13:08
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.

1 participant