Skip to content

Conversation

@stroiman
Copy link
Member

When a process running in a separate go-routine, e.g., a fetch request is represented by a promise in JS, the result of the process is enqueued on the task queue of the main go routine.

However, converting the result from a native Go value to a JavaScript value should have occurred in the main go-routine, not the concurrent task. This could cause concurrent calls to the same JavaScript context, which isn't allowed.

When a process running in a separate go-routine, e.g., a fetch request
is represented by a promise in JS, the result of the process is enqueued
on the task queue of the main go routine.

However, converting the result from a native Go value to a JavaScript
value should have occurred in the main go-routine, not the concurrent
task. This could cause concurrent calls to the same JavaScript context,
which isn't allowed.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

Walkthrough

This PR introduces a scoped clock execution model: Clock gains a stack field, private enter/exit helpers and a new public Do(f func() error) method while Tick() is renamed to private tick(). Callers across scripting engines and modules were updated to use do wrappers that delegate to Clock.Do when present, removing prior explicit tick/error-joining patterns. It inlines promise encoder logic (removing EncodePromiseFunc), fixes a syntax error in an event handler, adds a microtask test, and enables the Go race detector in tests.

Possibly related PRs

  • gost-dom/browser PR 244: Modifies the clock package and public clock API surface, overlapping changes to Clock and its behavior.
  • gost-dom/browser PR 219: Updates event-loop/clock APIs and integrates new clock primitives, touching the same scheduling surface now using Do.
  • gost-dom/browser PR 243: Refactors timers and microtask scheduling (setTimeout/setInterval/queueMicrotask), closely related to the clock and microtask handling changes.
🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: Fix race condition' clearly describes the main change—addressing a race condition in concurrent goroutine handling. It accurately reflects the core objective documented in the PR.
Description check ✅ Passed The description directly explains the race condition: Go→JavaScript value conversion was happening in a background goroutine instead of the main goroutine, which is unsafe. It clearly relates to the changeset that refactors event handling to use Clock.Do() for proper synchronization.

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


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
Contributor

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

Actionable comments posted: 3

Copy link
Contributor

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

Actionable comments posted: 1

Copy link
Contributor

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

Actionable comments posted: 3

Comment on lines +229 to +236
func (c *Clock) enter() { c.stack++ }
func (c *Clock) exit() error {
c.stack--
if c.stack > 0 {
return nil
}
return c.tick()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding a defensive check for unbalanced calls.

If exit() is ever called without a matching enter() (e.g., due to a future refactoring mistake), stack would become negative and tick() would still run. While the current code correctly pairs these via Do(), a defensive check could help catch bugs early during development.

🛡️ Optional: Add defensive assertion
 func (c *Clock) enter() { c.stack++ }
 func (c *Clock) exit() error {
 	c.stack--
+	if c.stack < 0 {
+		panic("gost-dom/clock: exit() called without matching enter()")
+	}
 	if c.stack > 0 {
 		return nil
 	}
 	return c.tick()
 }
📝 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.

Suggested change
func (c *Clock) enter() { c.stack++ }
func (c *Clock) exit() error {
c.stack--
if c.stack > 0 {
return nil
}
return c.tick()
}
func (c *Clock) enter() { c.stack++ }
func (c *Clock) exit() error {
c.stack--
if c.stack < 0 {
panic("gost-dom/clock: exit() called without matching enter()")
}
if c.stack > 0 {
return nil
}
return c.tick()
}

Comment on lines 249 to +251
// Tick runs all tasks scheduled for immediate execution. This is synonymous
// with calling Advance(0).
func (c *Clock) Tick() error { return c.Advance(0) }
func (c *Clock) tick() error { return c.Advance(0) }
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor: Update comment to reflect the method is now private.

The comment still describes Tick as if it were a public API entry point. Since it's now private (tick), consider updating the comment to reflect its internal nature, or removing it since callers should use Do() instead.

📝 Suggested comment update
-// Tick runs all tasks scheduled for immediate execution. This is synonymous
-// with calling Advance(0).
+// tick runs all tasks scheduled for immediate execution (equivalent to Advance(0)).
+// This is called internally by exit() when the outermost Do() completes.
 func (c *Clock) tick() error { return c.Advance(0) }

Comment on lines +20 to +33
return m.ctx.do(func() error {
m.ctx.logger().Debug("Evaluate module", "vm", m.ctx.vm)
p := m.record.Evaluate(m.ctx.vm)

if p.State() != sobek.PromiseStateFulfilled {
return p.Result().Export().(error)
}
return m.ctx.tick()
if p.State() != sobek.PromiseStateFulfilled {
res := p.Result().Export()
err, ok := res.(error)
if !ok {
err = fmt.Errorf("run module: %v", res)
}
return err
}
return nil
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the file and understand the repository structure
find . -name "sobek_module.go" -type f | head -20

Repository: gost-dom/browser

Length of output: 99


🏁 Script executed:

#!/bin/bash
# List relevant files in the scripting/sobekengine directory
git ls-files | grep -E "(sobek|script)" | head -30

Repository: gost-dom/browser

Length of output: 1678


🏁 Script executed:

#!/bin/bash
# Inspect the sobek_module.go file around lines 20-33
find . -name "sobek_module.go" -type f -exec cat -n {} \; | head -50

Repository: gost-dom/browser

Length of output: 907


🏁 Script executed:

#!/bin/bash
# Find the scriptContext type and its do() method
rg -n "func.*\(.*scriptContext.*\)\s*do\(" scripting/sobekengine/ -A 10

Repository: gost-dom/browser

Length of output: 859


🏁 Script executed:

#!/bin/bash
# Search for scriptContext definition
rg -n "type scriptContext" scripting/sobekengine/ -A 20

Repository: gost-dom/browser

Length of output: 1530


🏁 Script executed:

#!/bin/bash
# Search for Clock.Do method or related event loop handling
rg -n "Clock|EventLoop|RunTicks|Drain" scripting/sobekengine/ -A 5 -B 2

Repository: gost-dom/browser

Length of output: 1617


🏁 Script executed:

#!/bin/bash
# Look for all go files in sobekengine to understand the structure
git ls-files scripting/sobekengine/ | grep "\.go$"

Repository: gost-dom/browser

Length of output: 668


🏁 Script executed:

#!/bin/bash
# Find the Clock type and its Do method implementation
find . -name "clock.go" -o -name "clock*.go" | grep -v test | xargs cat -n 2>/dev/null | head -100

Repository: gost-dom/browser

Length of output: 4604


🏁 Script executed:

#!/bin/bash
# Search for Clock.Do method definition across the codebase
rg -n "func.*Clock.*Do\(|func \(c \*Clock\)" --type=go -A 15

Repository: gost-dom/browser

Length of output: 15141


🏁 Script executed:

#!/bin/bash
# Look for the clock package
git ls-files | grep clock | grep -v test | grep "\.go$"

Repository: gost-dom/browser

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Search for tests or usage patterns involving module evaluation
rg -n "sobekModule|Run\(\)|Evaluate.*module" --type=go -B 2 -A 3 | head -60

Repository: gost-dom/browser

Length of output: 3260


🏁 Script executed:

#!/bin/bash
# Look for any handling of PromiseStatePending or promise settlement patterns
rg -n "PromiseState|PromisePending|moduleRecord\.Evaluate" --type=go -B 2 -A 2

Repository: gost-dom/browser

Length of output: 404


🏁 Script executed:

#!/bin/bash
# Check if there are comments or tests explaining expected module behavior
rg -n "top.?level.*await|module.*settle|async.*module" --type=go -i | head -30

Repository: gost-dom/browser

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Check for tests related to module evaluation
find . -name "*sobek*test.go" -o -name "*module*test.go" | xargs grep -l "module\|Run\|Evaluate" 2>/dev/null

Repository: gost-dom/browser

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Look at the sobek_test.go file to see how modules are tested
cat -n scripting/sobekengine/sobek_test.go | head -100

Repository: gost-dom/browser

Length of output: 1314


🏁 Script executed:

#!/bin/bash
# Search for any comments or documentation about module expectations in sobek_module.go
cat -n scripting/sobekengine/sobek_module.go

Repository: gost-dom/browser

Length of output: 907


🏁 Script executed:

#!/bin/bash
# Check if Sobek documentation or examples mention promise settlement
rg -n "Evaluate|Promise|module" scripting/sobekengine/ --type=go | grep -i "comment\|doc\|test" | head -20

Repository: gost-dom/browser

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Look at scripttests to understand what modules are being tested
find . -path "*/scripttests*" -name "*.go" | head -10

Repository: gost-dom/browser

Length of output: 592


🏁 Script executed:

#!/bin/bash
# Check the module test suite to see what module behavior is expected
rg -n "RunModuleSuite|TestESM" scripting/internal/ --type=go -A 5

Repository: gost-dom/browser

Length of output: 625


🏁 Script executed:

#!/bin/bash
# Search for any top-level await or async module handling in tests
rg -n "await|async|top.?level" scripting/internal/ --type=go | head -20

Repository: gost-dom/browser

Length of output: 701


🏁 Script executed:

#!/bin/bash
# Get the full module test suite to see what scenarios are tested
cat -n scripting/internal/scripttests/modules_test_suite.go | head -200

Repository: gost-dom/browser

Length of output: 6960


🏁 Script executed:

#!/bin/bash
# Check if sobek.ModuleRecord.Evaluate ever returns a pending promise, or if it always settles synchronously
rg -n "ModuleRecord\|\.Evaluate" scripting/sobekengine/ --type=go -B 2 -A 5

Repository: gost-dom/browser

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Look for references to Sobek library or documentation about module evaluation
rg -n "github.com/grafana/sobek" . --type=go | grep -i "doc\|comment\|import" | head -10

Repository: gost-dom/browser

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Search for any documentation or issues about async modules or top-level await
rg -n "top.*level|async.*module|awaiting.*module" . --type=go --type=md | head -20

Repository: gost-dom/browser

Length of output: 161


🏁 Script executed:

#!/bin/bash
# Check V8 module implementation for comparison
cat -n scripting/v8engine/module.go | head -50

Repository: gost-dom/browser

Length of output: 1476


🏁 Script executed:

#!/bin/bash
# See the full awaitPromise implementation in V8
cat -n scripting/v8engine/module.go

Repository: gost-dom/browser

Length of output: 2421


🏁 Script executed:

#!/bin/bash
# Check if there's a similar awaiting mechanism for Sobek
rg -n "awaitPromise|await.*Promise|promise.*settle" scripting/sobekengine/ --type=go -A 5

Repository: gost-dom/browser

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Look at how Sobek handles promises in general
rg -n "Promise|promise" scripting/sobekengine/ --type=go | head -30

Repository: gost-dom/browser

Length of output: 1121


🏁 Script executed:

#!/bin/bash
# Check Sobek documentation/issues about module evaluation and promise states
rg -n "PromiseState|Fulfilled|Pending|Rejected" scripting/sobekengine/ --type=go

Repository: gost-dom/browser

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Verify if there's any mechanism in Sobek to wait for promises like V8's PerformMicrotaskCheckpoint
rg -n "Microtask|checkpoint|drain" scripting/sobekengine/ --type=go

Repository: gost-dom/browser

Length of output: 42


Pending promise incorrectly treated as error—module evaluation does not await promise settlement.

The promise state is checked synchronously within Clock.Do() before microtasks are drained. For modules with top-level await (which return a pending promise), the check at line 24 will treat PromiseStatePending as a failure.

Compare to the V8 implementation (scripting/v8engine/module.go), which explicitly awaits the promise via awaitPromise() to allow settlement before returning. The Sobek implementation lacks this promise-awaiting mechanism and will reject async modules.

Recommend adding explicit promise settlement logic similar to V8, or ensuring Clock.Do() drains microtasks before the state check occurs.

@github-actions github-actions bot merged commit 6f6043d into main Jan 28, 2026
5 checks passed
@github-actions github-actions bot deleted the fix/microtasks branch January 28, 2026 16:48
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.

2 participants