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 }