1
0
Fork 0

Adding upstream version 0.0~git20250409.f7acab6.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-22 11:36:18 +02:00
parent b9b5d88025
commit 21b930d007
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
51 changed files with 11229 additions and 0 deletions

514
eventloop/eventloop.go Normal file
View file

@ -0,0 +1,514 @@
package eventloop
import (
"sync"
"sync/atomic"
"time"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/console"
"github.com/dop251/goja_nodejs/require"
)
type job struct {
cancel func() bool
fn func()
idx int
cancelled bool
}
type Timer struct {
job
timer *time.Timer
}
type Interval struct {
job
ticker *time.Ticker
stopChan chan struct{}
}
type Immediate struct {
job
}
type EventLoop struct {
vm *goja.Runtime
jobChan chan func()
jobs []*job
jobCount int32
canRun int32
auxJobsLock sync.Mutex
wakeupChan chan struct{}
auxJobsSpare, auxJobs []func()
stopLock sync.Mutex
stopCond *sync.Cond
running bool
terminated bool
enableConsole bool
registry *require.Registry
}
func NewEventLoop(opts ...Option) *EventLoop {
vm := goja.New()
loop := &EventLoop{
vm: vm,
jobChan: make(chan func()),
wakeupChan: make(chan struct{}, 1),
enableConsole: true,
}
loop.stopCond = sync.NewCond(&loop.stopLock)
for _, opt := range opts {
opt(loop)
}
if loop.registry == nil {
loop.registry = new(require.Registry)
}
loop.registry.Enable(vm)
if loop.enableConsole {
console.Enable(vm)
}
vm.Set("setTimeout", loop.setTimeout)
vm.Set("setInterval", loop.setInterval)
vm.Set("setImmediate", loop.setImmediate)
vm.Set("clearTimeout", loop.clearTimeout)
vm.Set("clearInterval", loop.clearInterval)
vm.Set("clearImmediate", loop.clearImmediate)
return loop
}
type Option func(*EventLoop)
// EnableConsole controls whether the "console" module is loaded into
// the runtime used by the loop. By default, loops are created with
// the "console" module loaded, pass EnableConsole(false) to
// NewEventLoop to disable this behavior.
func EnableConsole(enableConsole bool) Option {
return func(loop *EventLoop) {
loop.enableConsole = enableConsole
}
}
func WithRegistry(registry *require.Registry) Option {
return func(loop *EventLoop) {
loop.registry = registry
}
}
func (loop *EventLoop) schedule(call goja.FunctionCall, repeating bool) goja.Value {
if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
delay := call.Argument(1).ToInteger()
var args []goja.Value
if len(call.Arguments) > 2 {
args = append(args, call.Arguments[2:]...)
}
f := func() { fn(nil, args...) }
loop.jobCount++
var job *job
var ret goja.Value
if repeating {
interval := loop.newInterval(f)
interval.start(loop, time.Duration(delay)*time.Millisecond)
job = &interval.job
ret = loop.vm.ToValue(interval)
} else {
timeout := loop.newTimeout(f)
timeout.start(loop, time.Duration(delay)*time.Millisecond)
job = &timeout.job
ret = loop.vm.ToValue(timeout)
}
job.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, job)
return ret
}
return nil
}
func (loop *EventLoop) setTimeout(call goja.FunctionCall) goja.Value {
return loop.schedule(call, false)
}
func (loop *EventLoop) setInterval(call goja.FunctionCall) goja.Value {
return loop.schedule(call, true)
}
func (loop *EventLoop) setImmediate(call goja.FunctionCall) goja.Value {
if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
var args []goja.Value
if len(call.Arguments) > 1 {
args = append(args, call.Arguments[1:]...)
}
f := func() { fn(nil, args...) }
loop.jobCount++
return loop.vm.ToValue(loop.addImmediate(f))
}
return nil
}
// SetTimeout schedules to run the specified function in the context
// of the loop as soon as possible after the specified timeout period.
// SetTimeout returns a Timer which can be passed to ClearTimeout.
// The instance of goja.Runtime that is passed to the function and any Values derived
// from it must not be used outside the function. SetTimeout is
// safe to call inside or outside the loop.
// If the loop is terminated (see Terminate()) returns nil.
func (loop *EventLoop) SetTimeout(fn func(*goja.Runtime), timeout time.Duration) *Timer {
t := loop.newTimeout(func() { fn(loop.vm) })
if loop.addAuxJob(func() {
t.start(loop, timeout)
loop.jobCount++
t.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, &t.job)
}) {
return t
}
return nil
}
// ClearTimeout cancels a Timer returned by SetTimeout if it has not run yet.
// ClearTimeout is safe to call inside or outside the loop.
func (loop *EventLoop) ClearTimeout(t *Timer) {
loop.addAuxJob(func() {
loop.clearTimeout(t)
})
}
// SetInterval schedules to repeatedly run the specified function in
// the context of the loop as soon as possible after every specified
// timeout period. SetInterval returns an Interval which can be
// passed to ClearInterval. The instance of goja.Runtime that is passed to the
// function and any Values derived from it must not be used outside
// the function. SetInterval is safe to call inside or outside the
// loop.
// If the loop is terminated (see Terminate()) returns nil.
func (loop *EventLoop) SetInterval(fn func(*goja.Runtime), timeout time.Duration) *Interval {
i := loop.newInterval(func() { fn(loop.vm) })
if loop.addAuxJob(func() {
i.start(loop, timeout)
loop.jobCount++
i.idx = len(loop.jobs)
loop.jobs = append(loop.jobs, &i.job)
}) {
return i
}
return nil
}
// ClearInterval cancels an Interval returned by SetInterval.
// ClearInterval is safe to call inside or outside the loop.
func (loop *EventLoop) ClearInterval(i *Interval) {
loop.addAuxJob(func() {
loop.clearInterval(i)
})
}
func (loop *EventLoop) setRunning() {
loop.stopLock.Lock()
defer loop.stopLock.Unlock()
if loop.running {
panic("Loop is already started")
}
loop.running = true
atomic.StoreInt32(&loop.canRun, 1)
loop.auxJobsLock.Lock()
loop.terminated = false
loop.auxJobsLock.Unlock()
}
// Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run
// after which it stops the loop and returns.
// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used
// outside the function.
// Do NOT use this function while the loop is already running. Use RunOnLoop() instead.
// If the loop is already started it will panic.
func (loop *EventLoop) Run(fn func(*goja.Runtime)) {
loop.setRunning()
fn(loop.vm)
loop.run(false)
}
// Start the event loop in the background. The loop continues to run until Stop() is called.
// If the loop is already started it will panic.
func (loop *EventLoop) Start() {
loop.setRunning()
go loop.run(true)
}
// StartInForeground starts the event loop in the current goroutine. The loop continues to run until Stop() is called.
// If the loop is already started it will panic.
// Use this instead of Start if you want to recover from panics that may occur while calling native Go functions from
// within setInterval and setTimeout callbacks.
func (loop *EventLoop) StartInForeground() {
loop.setRunning()
loop.run(true)
}
// Stop the loop that was started with Start(). After this function returns there will be no more jobs executed
// by the loop. It is possible to call Start() or Run() again after this to resume the execution.
// Note, it does not cancel active timeouts (use Terminate() instead if you want this).
// It is not allowed to run Start() (or Run()) and Stop() or Terminate() concurrently.
// Calling Stop() on a non-running loop has no effect.
// It is not allowed to call Stop() from the loop, because it is synchronous and cannot complete until the loop
// is not running any jobs. Use StopNoWait() instead.
// return number of jobs remaining
func (loop *EventLoop) Stop() int {
loop.stopLock.Lock()
for loop.running {
atomic.StoreInt32(&loop.canRun, 0)
loop.wakeup()
loop.stopCond.Wait()
}
loop.stopLock.Unlock()
return int(loop.jobCount)
}
// StopNoWait tells the loop to stop and returns immediately. Can be used inside the loop. Calling it on a
// non-running loop has no effect.
func (loop *EventLoop) StopNoWait() {
loop.stopLock.Lock()
if loop.running {
atomic.StoreInt32(&loop.canRun, 0)
loop.wakeup()
}
loop.stopLock.Unlock()
}
// Terminate stops the loop and clears all active timeouts and intervals. After it returns there are no
// active timers or goroutines associated with the loop. Any attempt to submit a task (by using RunOnLoop(),
// SetTimeout() or SetInterval()) will not succeed.
// After being terminated the loop can be restarted again by using Start() or Run().
// This method must not be called concurrently with Stop*(), Start(), or Run().
func (loop *EventLoop) Terminate() {
loop.Stop()
loop.auxJobsLock.Lock()
loop.terminated = true
loop.auxJobsLock.Unlock()
loop.runAux()
for i := 0; i < len(loop.jobs); i++ {
job := loop.jobs[i]
if !job.cancelled {
job.cancelled = true
if job.cancel() {
loop.removeJob(job)
i--
}
}
}
for len(loop.jobs) > 0 {
(<-loop.jobChan)()
}
}
// RunOnLoop schedules to run the specified function in the context of the loop as soon as possible.
// The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop())
// The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used
// outside the function. It is safe to call inside or outside the loop.
// Returns true on success or false if the loop is terminated (see Terminate()).
func (loop *EventLoop) RunOnLoop(fn func(*goja.Runtime)) bool {
return loop.addAuxJob(func() { fn(loop.vm) })
}
func (loop *EventLoop) runAux() {
loop.auxJobsLock.Lock()
jobs := loop.auxJobs
loop.auxJobs = loop.auxJobsSpare
loop.auxJobsLock.Unlock()
for i, job := range jobs {
job()
jobs[i] = nil
}
loop.auxJobsSpare = jobs[:0]
}
func (loop *EventLoop) run(inBackground bool) {
loop.runAux()
if inBackground {
loop.jobCount++
}
LOOP:
for loop.jobCount > 0 {
select {
case job := <-loop.jobChan:
job()
case <-loop.wakeupChan:
loop.runAux()
if atomic.LoadInt32(&loop.canRun) == 0 {
break LOOP
}
}
}
if inBackground {
loop.jobCount--
}
loop.stopLock.Lock()
loop.running = false
loop.stopLock.Unlock()
loop.stopCond.Broadcast()
}
func (loop *EventLoop) wakeup() {
select {
case loop.wakeupChan <- struct{}{}:
default:
}
}
func (loop *EventLoop) addAuxJob(fn func()) bool {
loop.auxJobsLock.Lock()
if loop.terminated {
loop.auxJobsLock.Unlock()
return false
}
loop.auxJobs = append(loop.auxJobs, fn)
loop.auxJobsLock.Unlock()
loop.wakeup()
return true
}
func (loop *EventLoop) newTimeout(f func()) *Timer {
t := &Timer{
job: job{fn: f},
}
t.cancel = t.doCancel
return t
}
func (t *Timer) start(loop *EventLoop, timeout time.Duration) {
t.timer = time.AfterFunc(timeout, func() {
loop.jobChan <- func() {
loop.doTimeout(t)
}
})
}
func (loop *EventLoop) newInterval(f func()) *Interval {
i := &Interval{
job: job{fn: f},
stopChan: make(chan struct{}),
}
i.cancel = i.doCancel
return i
}
func (i *Interval) start(loop *EventLoop, timeout time.Duration) {
// https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args
if timeout <= 0 {
timeout = time.Millisecond
}
i.ticker = time.NewTicker(timeout)
go i.run(loop)
}
func (loop *EventLoop) addImmediate(f func()) *Immediate {
i := &Immediate{
job: job{fn: f},
}
loop.addAuxJob(func() {
loop.doImmediate(i)
})
return i
}
func (loop *EventLoop) doTimeout(t *Timer) {
loop.removeJob(&t.job)
if !t.cancelled {
t.cancelled = true
loop.jobCount--
t.fn()
}
}
func (loop *EventLoop) doInterval(i *Interval) {
if !i.cancelled {
i.fn()
}
}
func (loop *EventLoop) doImmediate(i *Immediate) {
if !i.cancelled {
i.cancelled = true
loop.jobCount--
i.fn()
}
}
func (loop *EventLoop) clearTimeout(t *Timer) {
if t != nil && !t.cancelled {
t.cancelled = true
loop.jobCount--
if t.doCancel() {
loop.removeJob(&t.job)
}
}
}
func (loop *EventLoop) clearInterval(i *Interval) {
if i != nil && !i.cancelled {
i.cancelled = true
loop.jobCount--
i.doCancel()
}
}
func (loop *EventLoop) removeJob(job *job) {
idx := job.idx
if idx < 0 {
return
}
if idx < len(loop.jobs)-1 {
loop.jobs[idx] = loop.jobs[len(loop.jobs)-1]
loop.jobs[idx].idx = idx
}
loop.jobs[len(loop.jobs)-1] = nil
loop.jobs = loop.jobs[:len(loop.jobs)-1]
job.idx = -1
}
func (loop *EventLoop) clearImmediate(i *Immediate) {
if i != nil && !i.cancelled {
i.cancelled = true
loop.jobCount--
}
}
func (i *Interval) doCancel() bool {
close(i.stopChan)
return false
}
func (t *Timer) doCancel() bool {
return t.timer.Stop()
}
func (i *Interval) run(loop *EventLoop) {
L:
for {
select {
case <-i.stopChan:
i.ticker.Stop()
break L
case <-i.ticker.C:
loop.jobChan <- func() {
loop.doInterval(i)
}
}
}
loop.jobChan <- func() {
loop.removeJob(&i.job)
}
}

641
eventloop/eventloop_test.go Normal file
View file

@ -0,0 +1,641 @@
package eventloop
import (
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/dop251/goja"
"go.uber.org/goleak"
)
func TestRun(t *testing.T) {
t.Parallel()
const SCRIPT = `
var calledAt;
setTimeout(function() {
calledAt = now();
}, 1000);
`
loop := NewEventLoop()
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
startTime := time.Now()
loop.Run(func(vm *goja.Runtime) {
vm.Set("now", time.Now)
_, err = vm.RunProgram(prg)
})
if err != nil {
t.Fatal(err)
}
var calledAt time.Time
loop.Run(func(vm *goja.Runtime) {
err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
})
if err != nil {
t.Fatal(err)
}
if calledAt.IsZero() {
t.Fatal("Not called")
}
if dur := calledAt.Sub(startTime); dur < time.Second {
t.Fatal(dur)
}
}
func TestStart(t *testing.T) {
t.Parallel()
const SCRIPT = `
var calledAt;
setTimeout(function() {
calledAt = now();
}, 1000);
`
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop := NewEventLoop()
startTime := time.Now()
loop.Start()
loop.RunOnLoop(func(vm *goja.Runtime) {
vm.Set("now", time.Now)
vm.RunProgram(prg)
})
time.Sleep(2 * time.Second)
if remainingJobs := loop.Stop(); remainingJobs != 0 {
t.Fatal(remainingJobs)
}
var calledAt time.Time
loop.Run(func(vm *goja.Runtime) {
err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
})
if err != nil {
t.Fatal(err)
}
if calledAt.IsZero() {
t.Fatal("Not called")
}
if dur := calledAt.Sub(startTime); dur < time.Second {
t.Fatal(dur)
}
}
func TestStartInForeground(t *testing.T) {
t.Parallel()
const SCRIPT = `
var calledAt;
setTimeout(function() {
calledAt = now();
}, 1000);
`
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop := NewEventLoop()
startTime := time.Now()
go loop.StartInForeground()
loop.RunOnLoop(func(vm *goja.Runtime) {
vm.Set("now", time.Now)
vm.RunProgram(prg)
})
time.Sleep(2 * time.Second)
if remainingJobs := loop.Stop(); remainingJobs != 0 {
t.Fatal(remainingJobs)
}
var calledAt time.Time
loop.Run(func(vm *goja.Runtime) {
err = vm.ExportTo(vm.Get("calledAt"), &calledAt)
})
if err != nil {
t.Fatal(err)
}
if calledAt.IsZero() {
t.Fatal("Not called")
}
if dur := calledAt.Sub(startTime); dur < time.Second {
t.Fatal(dur)
}
}
func TestInterval(t *testing.T) {
t.Parallel()
const SCRIPT = `
var count = 0;
var t = setInterval(function(times) {
console.log("tick");
if (++count > times) {
clearInterval(t);
}
}, 1000, 2);
console.log("Started");
`
loop := NewEventLoop()
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop.Run(func(vm *goja.Runtime) {
_, err = vm.RunProgram(prg)
})
if err != nil {
t.Fatal(err)
}
var count int64
loop.Run(func(vm *goja.Runtime) {
count = vm.Get("count").ToInteger()
})
if count != 3 {
t.Fatal(count)
}
}
func TestImmediate(t *testing.T) {
t.Parallel()
const SCRIPT = `
let log = [];
function cb(arg) {
log.push(arg);
}
var i;
var t = setImmediate(function() {
cb("tick");
setImmediate(cb, "tick 2");
i = setImmediate(cb, "should not run")
});
setImmediate(function() {
clearImmediate(i);
});
cb("Started");
`
loop := NewEventLoop()
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop.Run(func(vm *goja.Runtime) {
_, err = vm.RunProgram(prg)
})
if err != nil {
t.Fatal(err)
}
loop.Run(func(vm *goja.Runtime) {
_, err = vm.RunString(`
if (log.length != 3) {
throw new Error("Invalid log length: " + log);
}
if (log[0] !== "Started" || log[1] !== "tick" || log[2] !== "tick 2") {
throw new Error("Invalid log: " + log);
}
`)
})
if err != nil {
t.Fatal(err)
}
}
func TestRunNoSchedule(t *testing.T) {
loop := NewEventLoop()
fired := false
loop.Run(func(vm *goja.Runtime) { // should not hang
fired = true
// do not schedule anything
})
if !fired {
t.Fatal("Not fired")
}
}
func TestRunWithConsole(t *testing.T) {
const SCRIPT = `
console.log("Started");
`
loop := NewEventLoop()
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop.Run(func(vm *goja.Runtime) {
_, err = vm.RunProgram(prg)
})
if err != nil {
t.Fatal("Call to console.log generated an error", err)
}
loop = NewEventLoop(EnableConsole(true))
prg, err = goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop.Run(func(vm *goja.Runtime) {
_, err = vm.RunProgram(prg)
})
if err != nil {
t.Fatal("Call to console.log generated an error", err)
}
}
func TestRunNoConsole(t *testing.T) {
const SCRIPT = `
console.log("Started");
`
loop := NewEventLoop(EnableConsole(false))
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop.Run(func(vm *goja.Runtime) {
_, err = vm.RunProgram(prg)
})
if err == nil {
t.Fatal("Call to console.log did not generate an error", err)
}
}
func TestClearIntervalRace(t *testing.T) {
t.Parallel()
const SCRIPT = `
console.log("calling setInterval");
var t = setInterval(function() {
console.log("tick");
}, 500);
console.log("calling sleep");
sleep(2000);
console.log("calling clearInterval");
clearInterval(t);
`
loop := NewEventLoop()
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
// Should not hang
loop.Run(func(vm *goja.Runtime) {
vm.Set("sleep", func(ms int) {
<-time.After(time.Duration(ms) * time.Millisecond)
})
vm.RunProgram(prg)
})
}
func TestNativeTimeout(t *testing.T) {
t.Parallel()
fired := false
loop := NewEventLoop()
loop.SetTimeout(func(*goja.Runtime) {
fired = true
}, 1*time.Second)
loop.Run(func(*goja.Runtime) {
// do not schedule anything
})
if !fired {
t.Fatal("Not fired")
}
}
func TestNativeClearTimeout(t *testing.T) {
t.Parallel()
fired := false
loop := NewEventLoop()
timer := loop.SetTimeout(func(*goja.Runtime) {
fired = true
}, 2*time.Second)
loop.SetTimeout(func(*goja.Runtime) {
loop.ClearTimeout(timer)
}, 1*time.Second)
loop.Run(func(*goja.Runtime) {
// do not schedule anything
})
if fired {
t.Fatal("Cancelled timer fired!")
}
}
func TestNativeInterval(t *testing.T) {
t.Parallel()
count := 0
loop := NewEventLoop()
var i *Interval
i = loop.SetInterval(func(*goja.Runtime) {
t.Log("tick")
count++
if count > 2 {
loop.ClearInterval(i)
}
}, 1*time.Second)
loop.Run(func(*goja.Runtime) {
// do not schedule anything
})
if count != 3 {
t.Fatal("Expected interval to fire 3 times, got", count)
}
}
func TestNativeClearInterval(t *testing.T) {
t.Parallel()
count := 0
loop := NewEventLoop()
loop.Run(func(*goja.Runtime) {
i := loop.SetInterval(func(*goja.Runtime) {
t.Log("tick")
count++
}, 500*time.Millisecond)
<-time.After(2 * time.Second)
loop.ClearInterval(i)
})
if count != 0 {
t.Fatal("Expected interval to fire 0 times, got", count)
}
}
func TestSetAndClearOnStoppedLoop(t *testing.T) {
t.Parallel()
loop := NewEventLoop()
timeout := loop.SetTimeout(func(runtime *goja.Runtime) {
panic("must not run")
}, 1*time.Millisecond)
loop.ClearTimeout(timeout)
loop.Start()
time.Sleep(10 * time.Millisecond)
loop.Terminate()
}
func TestSetTimeoutConcurrent(t *testing.T) {
t.Parallel()
loop := NewEventLoop()
loop.Start()
ch := make(chan struct{}, 1)
loop.SetTimeout(func(*goja.Runtime) {
ch <- struct{}{}
}, 100*time.Millisecond)
<-ch
loop.Stop()
}
func TestClearTimeoutConcurrent(t *testing.T) {
t.Parallel()
loop := NewEventLoop()
loop.Start()
timer := loop.SetTimeout(func(*goja.Runtime) {
}, 100*time.Millisecond)
loop.ClearTimeout(timer)
loop.Stop()
if c := loop.jobCount; c != 0 {
t.Fatalf("jobCount: %d", c)
}
}
func TestClearIntervalConcurrent(t *testing.T) {
t.Parallel()
loop := NewEventLoop()
loop.Start()
ch := make(chan struct{}, 1)
i := loop.SetInterval(func(*goja.Runtime) {
ch <- struct{}{}
}, 500*time.Millisecond)
<-ch
loop.ClearInterval(i)
loop.Stop()
if c := loop.jobCount; c != 0 {
t.Fatalf("jobCount: %d", c)
}
}
func TestRunOnStoppedLoop(t *testing.T) {
t.Parallel()
loop := NewEventLoop()
var failed int32
done := make(chan struct{})
go func() {
for atomic.LoadInt32(&failed) == 0 {
loop.Start()
time.Sleep(10 * time.Millisecond)
loop.Stop()
}
}()
go func() {
for atomic.LoadInt32(&failed) == 0 {
loop.RunOnLoop(func(*goja.Runtime) {
if !loop.running {
atomic.StoreInt32(&failed, 1)
close(done)
return
}
})
time.Sleep(10 * time.Millisecond)
}
}()
select {
case <-done:
case <-time.After(5 * time.Second):
}
if atomic.LoadInt32(&failed) != 0 {
t.Fatal("running job on stopped loop")
}
}
func TestPromise(t *testing.T) {
t.Parallel()
const SCRIPT = `
let result;
const p = new Promise((resolve, reject) => {
setTimeout(() => {resolve("passed")}, 500);
});
p.then(value => {
result = value;
});
`
loop := NewEventLoop()
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop.Run(func(vm *goja.Runtime) {
_, err = vm.RunProgram(prg)
})
if err != nil {
t.Fatal(err)
}
loop.Run(func(vm *goja.Runtime) {
result := vm.Get("result")
if !result.SameAs(vm.ToValue("passed")) {
err = fmt.Errorf("unexpected result: %v", result)
}
})
if err != nil {
t.Fatal(err)
}
}
func TestPromiseNative(t *testing.T) {
t.Parallel()
const SCRIPT = `
let result;
p.then(value => {
result = value;
done();
});
`
loop := NewEventLoop()
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
ch := make(chan error)
loop.Start()
defer loop.Stop()
loop.RunOnLoop(func(vm *goja.Runtime) {
vm.Set("done", func() {
ch <- nil
})
p, resolve, _ := vm.NewPromise()
vm.Set("p", p)
_, err = vm.RunProgram(prg)
if err != nil {
ch <- err
return
}
go func() {
time.Sleep(500 * time.Millisecond)
loop.RunOnLoop(func(*goja.Runtime) {
resolve("passed")
})
}()
})
err = <-ch
if err != nil {
t.Fatal(err)
}
loop.RunOnLoop(func(vm *goja.Runtime) {
result := vm.Get("result")
if !result.SameAs(vm.ToValue("passed")) {
ch <- fmt.Errorf("unexpected result: %v", result)
} else {
ch <- nil
}
})
err = <-ch
if err != nil {
t.Fatal(err)
}
}
func TestEventLoop_StopNoWait(t *testing.T) {
t.Parallel()
loop := NewEventLoop()
var ran int32
loop.Run(func(runtime *goja.Runtime) {
loop.SetTimeout(func(*goja.Runtime) {
atomic.StoreInt32(&ran, 1)
}, 5*time.Second)
loop.SetTimeout(func(*goja.Runtime) {
loop.StopNoWait()
}, 500*time.Millisecond)
})
if atomic.LoadInt32(&ran) != 0 {
t.Fatal("ran != 0")
}
}
func TestEventLoop_ClearRunningTimeout(t *testing.T) {
t.Parallel()
const SCRIPT = `
var called = 0;
let aTimer;
function a() {
if (++called > 5) {
return;
}
if (aTimer) {
clearTimeout(aTimer);
}
console.log("ok");
aTimer = setTimeout(a, 500);
}
a();`
prg, err := goja.Compile("main.js", SCRIPT, false)
if err != nil {
t.Fatal(err)
}
loop := NewEventLoop()
loop.Run(func(vm *goja.Runtime) {
_, err = vm.RunProgram(prg)
})
if err != nil {
t.Fatal(err)
}
var called int64
loop.Run(func(vm *goja.Runtime) {
called = vm.Get("called").ToInteger()
})
if called != 6 {
t.Fatal(called)
}
}
func TestEventLoop_Terminate(t *testing.T) {
defer goleak.VerifyNone(t)
loop := NewEventLoop()
loop.Start()
interval := loop.SetInterval(func(vm *goja.Runtime) {}, 10*time.Millisecond)
time.Sleep(500 * time.Millisecond)
loop.ClearInterval(interval)
loop.Terminate()
if loop.SetTimeout(func(*goja.Runtime) {}, time.Millisecond) != nil {
t.Fatal("was able to SetTimeout()")
}
if loop.SetInterval(func(*goja.Runtime) {}, time.Millisecond) != nil {
t.Fatal("was able to SetInterval()")
}
if loop.RunOnLoop(func(*goja.Runtime) {}) {
t.Fatal("was able to RunOnLoop()")
}
ch := make(chan struct{})
loop.Start()
if !loop.RunOnLoop(func(runtime *goja.Runtime) {
close(ch)
}) {
t.Fatal("RunOnLoop() has failed after restart")
}
<-ch
loop.Terminate()
}