gitee.com/quant1x/pkg@v0.2.8/goja_nodejs/eventloop/eventloop.go (about)

     1  package eventloop
     2  
     3  import (
     4  	"sync"
     5  	"time"
     6  
     7  	"gitee.com/quant1x/pkg/goja"
     8  	"gitee.com/quant1x/pkg/goja_nodejs/console"
     9  	"gitee.com/quant1x/pkg/goja_nodejs/require"
    10  )
    11  
    12  type job struct {
    13  	cancelled bool
    14  	fn        func()
    15  }
    16  
    17  type Timer struct {
    18  	job
    19  	timer *time.Timer
    20  }
    21  
    22  type Interval struct {
    23  	job
    24  	ticker   *time.Ticker
    25  	stopChan chan struct{}
    26  }
    27  
    28  type EventLoop struct {
    29  	vm       *goja.Runtime
    30  	jobChan  chan func()
    31  	jobCount int32
    32  	canRun   bool
    33  
    34  	auxJobs     []func()
    35  	auxJobsLock sync.Mutex
    36  	wakeup      chan struct{}
    37  
    38  	stopCond *sync.Cond
    39  	running  bool
    40  
    41  	enableConsole bool
    42  }
    43  
    44  func NewEventLoop(opts ...Option) *EventLoop {
    45  	vm := goja.New()
    46  
    47  	loop := &EventLoop{
    48  		vm:            vm,
    49  		jobChan:       make(chan func()),
    50  		wakeup:        make(chan struct{}, 1),
    51  		stopCond:      sync.NewCond(&sync.Mutex{}),
    52  		enableConsole: true,
    53  	}
    54  
    55  	for _, opt := range opts {
    56  		opt(loop)
    57  	}
    58  
    59  	new(require.Registry).Enable(vm)
    60  	if loop.enableConsole {
    61  		console.Enable(vm)
    62  	}
    63  	vm.Set("setTimeout", loop.setTimeout)
    64  	vm.Set("setInterval", loop.setInterval)
    65  	vm.Set("clearTimeout", loop.clearTimeout)
    66  	vm.Set("clearInterval", loop.clearInterval)
    67  
    68  	return loop
    69  }
    70  
    71  type Option func(*EventLoop)
    72  
    73  // EnableConsole controls whether the "console" module is loaded into
    74  // the runtime used by the loop.  By default, loops are created with
    75  // the "console" module loaded, pass EnableConsole(false) to
    76  // NewEventLoop to disable this behavior.
    77  func EnableConsole(enableConsole bool) Option {
    78  	return func(loop *EventLoop) {
    79  		loop.enableConsole = enableConsole
    80  	}
    81  }
    82  
    83  func (loop *EventLoop) schedule(call goja.FunctionCall, repeating bool) goja.Value {
    84  	if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
    85  		delay := call.Argument(1).ToInteger()
    86  		var args []goja.Value
    87  		if len(call.Arguments) > 2 {
    88  			args = call.Arguments[2:]
    89  		}
    90  		f := func() { fn(nil, args...) }
    91  		loop.jobCount++
    92  		if repeating {
    93  			return loop.vm.ToValue(loop.addInterval(f, time.Duration(delay)*time.Millisecond))
    94  		} else {
    95  			return loop.vm.ToValue(loop.addTimeout(f, time.Duration(delay)*time.Millisecond))
    96  		}
    97  	}
    98  	return nil
    99  }
   100  
   101  func (loop *EventLoop) setTimeout(call goja.FunctionCall) goja.Value {
   102  	return loop.schedule(call, false)
   103  }
   104  
   105  func (loop *EventLoop) setInterval(call goja.FunctionCall) goja.Value {
   106  	return loop.schedule(call, true)
   107  }
   108  
   109  // SetTimeout schedules to run the specified function in the context
   110  // of the loop as soon as possible after the specified timeout period.
   111  // SetTimeout returns a Timer which can be passed to ClearTimeout.
   112  // The instance of goja.Runtime that is passed to the function and any Values derived
   113  // from it must not be used outside of the function. SetTimeout is
   114  // safe to call inside or outside of the loop.
   115  func (loop *EventLoop) SetTimeout(fn func(*goja.Runtime), timeout time.Duration) *Timer {
   116  	t := loop.addTimeout(func() { fn(loop.vm) }, timeout)
   117  	loop.addAuxJob(func() {
   118  		loop.jobCount++
   119  	})
   120  	return t
   121  }
   122  
   123  // ClearTimeout cancels a Timer returned by SetTimeout if it has not run yet.
   124  // ClearTimeout is safe to call inside or outside of the loop.
   125  func (loop *EventLoop) ClearTimeout(t *Timer) {
   126  	loop.addAuxJob(func() {
   127  		loop.clearTimeout(t)
   128  	})
   129  }
   130  
   131  // SetInterval schedules to repeatedly run the specified function in
   132  // the context of the loop as soon as possible after every specified
   133  // timeout period.  SetInterval returns an Interval which can be
   134  // passed to ClearInterval. The instance of goja.Runtime that is passed to the
   135  // function and any Values derived from it must not be used outside of
   136  // the function. SetInterval is safe to call inside or outside of the
   137  // loop.
   138  func (loop *EventLoop) SetInterval(fn func(*goja.Runtime), timeout time.Duration) *Interval {
   139  	i := loop.addInterval(func() { fn(loop.vm) }, timeout)
   140  	loop.addAuxJob(func() {
   141  		loop.jobCount++
   142  	})
   143  	return i
   144  }
   145  
   146  // ClearInterval cancels an Interval returned by SetInterval.
   147  // ClearInterval is safe to call inside or outside of the loop.
   148  func (loop *EventLoop) ClearInterval(i *Interval) {
   149  	loop.addAuxJob(func() {
   150  		loop.clearInterval(i)
   151  	})
   152  }
   153  
   154  func (loop *EventLoop) setRunning() {
   155  	loop.stopCond.L.Lock()
   156  	if loop.running {
   157  		panic("Loop is already started")
   158  	}
   159  	loop.running = true
   160  	loop.stopCond.L.Unlock()
   161  }
   162  
   163  // Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run
   164  // after which it stops the loop and returns.
   165  // The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used outside
   166  // of the function.
   167  // Do NOT use this function while the loop is already running. Use RunOnLoop() instead.
   168  // If the loop is already started it will panic.
   169  func (loop *EventLoop) Run(fn func(*goja.Runtime)) {
   170  	loop.setRunning()
   171  	fn(loop.vm)
   172  	loop.run(false)
   173  }
   174  
   175  // Start the event loop in the background. The loop continues to run until Stop() is called.
   176  // If the loop is already started it will panic.
   177  func (loop *EventLoop) Start() {
   178  	loop.setRunning()
   179  	go loop.run(true)
   180  }
   181  
   182  // Stop the loop that was started with Start(). After this function returns there will be no more jobs executed
   183  // by the loop. It is possible to call Start() or Run() again after this to resume the execution.
   184  // Note, it does not cancel active timeouts.
   185  // It is not allowed to run Start() and Stop() concurrently.
   186  // Calling Stop() on an already stopped loop or inside the loop will hang.
   187  func (loop *EventLoop) Stop() {
   188  	loop.jobChan <- func() {
   189  		loop.canRun = false
   190  	}
   191  
   192  	loop.stopCond.L.Lock()
   193  	for loop.running {
   194  		loop.stopCond.Wait()
   195  	}
   196  	loop.stopCond.L.Unlock()
   197  }
   198  
   199  // RunOnLoop schedules to run the specified function in the context of the loop as soon as possible.
   200  // The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop())
   201  // The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used outside
   202  // of the function. It is safe to call inside or outside of the loop.
   203  func (loop *EventLoop) RunOnLoop(fn func(*goja.Runtime)) {
   204  	loop.addAuxJob(func() { fn(loop.vm) })
   205  }
   206  
   207  func (loop *EventLoop) runAux() {
   208  	loop.auxJobsLock.Lock()
   209  	jobs := loop.auxJobs
   210  	loop.auxJobs = nil
   211  	loop.auxJobsLock.Unlock()
   212  	for _, job := range jobs {
   213  		job()
   214  	}
   215  }
   216  
   217  func (loop *EventLoop) run(inBackground bool) {
   218  	loop.canRun = true
   219  	loop.runAux()
   220  
   221  	for loop.canRun && (inBackground || loop.jobCount > 0) {
   222  		select {
   223  		case job := <-loop.jobChan:
   224  			job()
   225  			if loop.canRun {
   226  				select {
   227  				case <-loop.wakeup:
   228  					loop.runAux()
   229  				default:
   230  				}
   231  			}
   232  		case <-loop.wakeup:
   233  			loop.runAux()
   234  		}
   235  	}
   236  	loop.stopCond.L.Lock()
   237  	loop.running = false
   238  	loop.stopCond.L.Unlock()
   239  	loop.stopCond.Broadcast()
   240  }
   241  
   242  func (loop *EventLoop) addAuxJob(fn func()) {
   243  	loop.auxJobsLock.Lock()
   244  	loop.auxJobs = append(loop.auxJobs, fn)
   245  	loop.auxJobsLock.Unlock()
   246  	select {
   247  	case loop.wakeup <- struct{}{}:
   248  	default:
   249  	}
   250  }
   251  
   252  func (loop *EventLoop) addTimeout(f func(), timeout time.Duration) *Timer {
   253  	t := &Timer{
   254  		job: job{fn: f},
   255  	}
   256  	t.timer = time.AfterFunc(timeout, func() {
   257  		loop.jobChan <- func() {
   258  			loop.doTimeout(t)
   259  		}
   260  	})
   261  
   262  	return t
   263  }
   264  
   265  func (loop *EventLoop) addInterval(f func(), timeout time.Duration) *Interval {
   266  	// https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args
   267  	if timeout <= 0 {
   268  		timeout = time.Millisecond
   269  	}
   270  
   271  	i := &Interval{
   272  		job:      job{fn: f},
   273  		ticker:   time.NewTicker(timeout),
   274  		stopChan: make(chan struct{}),
   275  	}
   276  
   277  	go i.run(loop)
   278  	return i
   279  }
   280  
   281  func (loop *EventLoop) doTimeout(t *Timer) {
   282  	if !t.cancelled {
   283  		t.fn()
   284  		t.cancelled = true
   285  		loop.jobCount--
   286  	}
   287  }
   288  
   289  func (loop *EventLoop) doInterval(i *Interval) {
   290  	if !i.cancelled {
   291  		i.fn()
   292  	}
   293  }
   294  
   295  func (loop *EventLoop) clearTimeout(t *Timer) {
   296  	if t != nil && !t.cancelled {
   297  		t.timer.Stop()
   298  		t.cancelled = true
   299  		loop.jobCount--
   300  	}
   301  }
   302  
   303  func (loop *EventLoop) clearInterval(i *Interval) {
   304  	if i != nil && !i.cancelled {
   305  		i.cancelled = true
   306  		close(i.stopChan)
   307  		loop.jobCount--
   308  	}
   309  }
   310  
   311  func (i *Interval) run(loop *EventLoop) {
   312  L:
   313  	for {
   314  		select {
   315  		case <-i.stopChan:
   316  			i.ticker.Stop()
   317  			break L
   318  		case <-i.ticker.C:
   319  			loop.jobChan <- func() {
   320  				loop.doInterval(i)
   321  			}
   322  		}
   323  	}
   324  }