github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/system/scripts/eventloop/eventloop.go (about)

     1  // This file is part of the Smart Home
     2  // Program complex distribution https://github.com/e154/smart-home
     3  // Copyright (C) 2016-2023, Filippov Alex
     4  //
     5  // This library is free software: you can redistribute it and/or
     6  // modify it under the terms of the GNU Lesser General Public
     7  // License as published by the Free Software Foundation; either
     8  // version 3 of the License, or (at your option) any later version.
     9  //
    10  // This library is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    13  // Library General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Lesser General Public
    16  // License along with this library.  If not, see
    17  // <https://www.gnu.org/licenses/>.
    18  
    19  package eventloop
    20  
    21  import (
    22  	"time"
    23  
    24  	"github.com/dop251/goja"
    25  	"github.com/dop251/goja_nodejs/console"
    26  	"github.com/dop251/goja_nodejs/require"
    27  )
    28  
    29  type job struct {
    30  	goja.Callable
    31  	args      []goja.Value
    32  	cancelled bool
    33  }
    34  
    35  type timer struct {
    36  	job
    37  	timer *time.Timer
    38  }
    39  
    40  type interval struct {
    41  	job
    42  	ticker   *time.Ticker
    43  	stopChan chan struct{}
    44  }
    45  
    46  // EventLoop ...
    47  type EventLoop struct {
    48  	vm       *goja.Runtime
    49  	jobChan  chan func()
    50  	jobCount int32
    51  	running  bool
    52  }
    53  
    54  // NewEventLoop ...
    55  func NewEventLoop(vm *goja.Runtime) *EventLoop {
    56  
    57  	loop := &EventLoop{
    58  		vm:      vm,
    59  		jobChan: make(chan func()),
    60  	}
    61  
    62  	new(require.Registry).Enable(vm)
    63  	console.Enable(vm)
    64  	_ = vm.Set("setTimeout", loop.setTimeout)
    65  	_ = vm.Set("setInterval", loop.setInterval)
    66  	_ = vm.Set("clearTimeout", loop.clearTimeout)
    67  	_ = vm.Set("clearInterval", loop.clearInterval)
    68  
    69  	return loop
    70  }
    71  
    72  func (loop *EventLoop) schedule(call goja.FunctionCall, repeating bool) goja.Value {
    73  	if fn, ok := goja.AssertFunction(call.Argument(0)); ok {
    74  		delay := call.Argument(1).ToInteger()
    75  		var args []goja.Value
    76  		if len(call.Arguments) > 2 {
    77  			args = call.Arguments[2:]
    78  		}
    79  		if repeating {
    80  			return loop.vm.ToValue(loop.addInterval(fn, time.Duration(delay)*time.Millisecond, args))
    81  		} else {
    82  			return loop.vm.ToValue(loop.addTimeout(fn, time.Duration(delay)*time.Millisecond, args))
    83  		}
    84  	}
    85  	return nil
    86  }
    87  
    88  func (loop *EventLoop) setTimeout(call goja.FunctionCall) goja.Value {
    89  	return loop.schedule(call, false)
    90  }
    91  
    92  func (loop *EventLoop) setInterval(call goja.FunctionCall) goja.Value {
    93  	return loop.schedule(call, true)
    94  }
    95  
    96  // Run calls the specified function, starts the event loop and waits until there are no more delayed jobs to run
    97  // after which it stops the loop and returns.
    98  // The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used outside
    99  // of the function.
   100  // Do NOT use this function while the loop is already running. Use RunOnLoop() instead.
   101  func (loop *EventLoop) Run(fn func(*goja.Runtime)) {
   102  	fn(loop.vm)
   103  	loop.run()
   104  }
   105  
   106  // Start the event loop in the background. The loop continues to run until Stop() is called.
   107  func (loop *EventLoop) Start() {
   108  	go loop.runInBackground()
   109  }
   110  
   111  // Stop the loop that was started with Start(). After this function returns there will be no more jobs executed
   112  // by the loop. It is possible to call Start() or Run() again after this to resume the execution.
   113  // Note, it does not cancel active timeouts.
   114  func (loop *EventLoop) Stop() {
   115  	ch := make(chan struct{})
   116  
   117  	loop.jobChan <- func() {
   118  		loop.running = false
   119  		ch <- struct{}{}
   120  	}
   121  
   122  	<-ch
   123  }
   124  
   125  // RunOnLoop schedules to run the specified function in the context of the loop as soon as possible.
   126  // The order of the runs is preserved (i.e. the functions will be called in the same order as calls to RunOnLoop())
   127  // The instance of goja.Runtime that is passed to the function and any Values derived from it must not be used outside
   128  // of the function.
   129  func (loop *EventLoop) RunOnLoop(fn func(*goja.Runtime)) {
   130  	loop.jobChan <- func() {
   131  		fn(loop.vm)
   132  	}
   133  }
   134  
   135  func (loop *EventLoop) run() {
   136  	loop.running = true
   137  	for loop.running && loop.jobCount > 0 {
   138  		job, ok := <-loop.jobChan
   139  		if !ok {
   140  			break
   141  		}
   142  		job()
   143  	}
   144  }
   145  
   146  func (loop *EventLoop) runInBackground() {
   147  	loop.running = true
   148  	for job := range loop.jobChan {
   149  		job()
   150  		if !loop.running {
   151  			break
   152  		}
   153  	}
   154  }
   155  
   156  func (loop *EventLoop) addTimeout(f goja.Callable, timeout time.Duration, args []goja.Value) *timer {
   157  	t := &timer{
   158  		job: job{Callable: f, args: args},
   159  	}
   160  
   161  	t.timer = time.AfterFunc(timeout, func() {
   162  		loop.jobChan <- func() {
   163  			loop.doTimeout(t)
   164  		}
   165  	})
   166  
   167  	loop.jobCount++
   168  	return t
   169  }
   170  
   171  func (loop *EventLoop) addInterval(f goja.Callable, timeout time.Duration, args []goja.Value) *interval {
   172  	i := &interval{
   173  		job:      job{Callable: f, args: args},
   174  		ticker:   time.NewTicker(timeout),
   175  		stopChan: make(chan struct{}),
   176  	}
   177  
   178  	go i.run(loop)
   179  	loop.jobCount++
   180  	return i
   181  }
   182  
   183  func (loop *EventLoop) doTimeout(t *timer) {
   184  	if !t.cancelled {
   185  		_, _ = t.Callable(nil, t.args...)
   186  		t.cancelled = true
   187  		loop.jobCount--
   188  	}
   189  }
   190  
   191  func (loop *EventLoop) doInterval(i *interval) {
   192  	if !i.cancelled {
   193  		_, _ = i.Callable(nil, i.args...)
   194  	}
   195  }
   196  
   197  func (loop *EventLoop) clearTimeout(t *timer) {
   198  	if !t.cancelled {
   199  		t.timer.Stop()
   200  		t.cancelled = true
   201  		loop.jobCount--
   202  	}
   203  }
   204  
   205  func (loop *EventLoop) clearInterval(i *interval) {
   206  	if !i.cancelled {
   207  		i.cancelled = true
   208  		i.stopChan <- struct{}{}
   209  		loop.jobCount--
   210  	}
   211  }
   212  
   213  func (i *interval) run(loop *EventLoop) {
   214  	for {
   215  		select {
   216  		case <-i.stopChan:
   217  			i.ticker.Stop()
   218  			break
   219  		case <-i.ticker.C:
   220  			loop.jobChan <- func() {
   221  				loop.doInterval(i)
   222  			}
   223  		}
   224  	}
   225  }