Adding upstream version 0.0~git20250409.f7acab6.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
b9b5d88025
commit
21b930d007
51 changed files with 11229 additions and 0 deletions
514
eventloop/eventloop.go
Normal file
514
eventloop/eventloop.go
Normal 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
641
eventloop/eventloop_test.go
Normal 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()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue