| /* |
| * Pure Ecmascript eventloop example. |
| * |
| * Timer state handling is inefficient in this trivial example. Timers are |
| * kept in an array sorted by their expiry time which works well for expiring |
| * timers, but has O(n) insertion performance. A better implementation would |
| * use a heap or some other efficient structure for managing timers so that |
| * all operations (insert, remove, get nearest timer) have good performance. |
| * |
| * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Timers |
| */ |
| |
| /* |
| * Event loop |
| * |
| * Timers are sorted by 'target' property which indicates expiry time of |
| * the timer. The timer expiring next is last in the array, so that |
| * removals happen at the end, and inserts for timers expiring in the |
| * near future displace as few elements in the array as possible. |
| */ |
| |
| EventLoop = { |
| // timers |
| timers: [], // active timers, sorted (nearest expiry last) |
| expiring: null, // set to timer being expired (needs special handling in clearTimeout/clearInterval) |
| nextTimerId: 1, |
| minimumDelay: 1, |
| minimumWait: 1, |
| maximumWait: 60000, |
| maxExpirys: 10, |
| |
| // sockets |
| socketListening: {}, // fd -> callback |
| socketReading: {}, // fd -> callback |
| socketConnecting: {}, // fd -> callback |
| |
| // misc |
| exitRequested: false |
| }; |
| |
| EventLoop.dumpState = function() { |
| print('TIMER STATE:'); |
| this.timers.forEach(function(t) { |
| print(' ' + Duktape.enc('jx', t)); |
| }); |
| if (this.expiring) { |
| print(' EXPIRING: ' + Duktape.enc('jx', this.expiring)); |
| } |
| } |
| |
| // Get timer with lowest expiry time. Since the active timers list is |
| // sorted, it's always the last timer. |
| EventLoop.getEarliestTimer = function() { |
| var timers = this.timers; |
| n = timers.length; |
| return (n > 0 ? timers[n - 1] : null); |
| } |
| |
| EventLoop.getEarliestWait = function() { |
| var t = this.getEarliestTimer(); |
| return (t ? t.target - Date.now() : null); |
| } |
| |
| EventLoop.insertTimer = function(timer) { |
| var timers = this.timers; |
| var i, n, t; |
| |
| /* |
| * Find 'i' such that we want to insert *after* timers[i] at index i+1. |
| * If no such timer, for-loop terminates with i-1, and we insert at -1+1=0. |
| */ |
| |
| n = timers.length; |
| for (i = n - 1; i >= 0; i--) { |
| t = timers[i]; |
| if (timer.target <= t.target) { |
| // insert after 't', to index i+1 |
| break; |
| } |
| } |
| |
| timers.splice(i + 1 /*start*/, 0 /*deleteCount*/, timer); |
| } |
| |
| // Remove timer/interval with a timer ID. The timer/interval can reside |
| // either on the active list or it may be an expired timer (this.expiring) |
| // whose user callback we're running when this function gets called. |
| EventLoop.removeTimerById = function(timer_id) { |
| var timers = this.timers; |
| var i, n, t; |
| |
| t = this.expiring; |
| if (t) { |
| if (t.id === timer_id) { |
| // Timer has expired and we're processing its callback. User |
| // callback has requested timer deletion. Mark removed, so |
| // that the timer is not reinserted back into the active list. |
| // This is actually a common case because an interval may very |
| // well cancel itself. |
| t.removed = true; |
| return; |
| } |
| } |
| |
| n = timers.length; |
| for (i = 0; i < n; i++) { |
| t = timers[i]; |
| if (t.id === timer_id) { |
| // Timer on active list: mark removed (not really necessary, but |
| // nice for dumping), and remove from active list. |
| t.removed = true; |
| this.timers.splice(i /*start*/, 1 /*deleteCount*/); |
| return; |
| } |
| } |
| |
| // no such ID, ignore |
| } |
| |
| EventLoop.processTimers = function() { |
| var now = Date.now(); |
| var timers = this.timers; |
| var sanity = this.maxExpirys; |
| var n, t; |
| |
| /* |
| * Here we must be careful with mutations: user callback may add and |
| * delete an arbitrary number of timers. |
| * |
| * Current solution is simple: check whether the timer at the end of |
| * the list has expired. If not, we're done. If it has expired, |
| * remove it from the active list, record it in this.expiring, and call |
| * the user callback. If user code deletes the this.expiring timer, |
| * there is special handling which just marks the timer deleted so |
| * it won't get inserted back into the active list. |
| * |
| * This process is repeated at most maxExpirys times to ensure we don't |
| * get stuck forever; user code could in principle add more and more |
| * already expired timers. |
| */ |
| |
| while (sanity-- > 0) { |
| // If exit requested, don't call any more callbacks. This allows |
| // a callback to do cleanups and request exit, and can be sure that |
| // no more callbacks are processed. |
| |
| if (this.exitRequested) { |
| //print('exit requested, exit'); |
| break; |
| } |
| |
| // Timers to expire? |
| |
| n = timers.length; |
| if (n <= 0) { |
| break; |
| } |
| t = timers[n - 1]; |
| if (now <= t.target) { |
| // Timer has not expired, and no other timer could have expired |
| // either because the list is sorted. |
| break; |
| } |
| timers.pop(); |
| |
| // Remove the timer from the active list and process it. The user |
| // callback may add new timers which is not a problem. The callback |
| // may also delete timers which is not a problem unless the timer |
| // being deleted is the timer whose callback we're running; this is |
| // why the timer is recorded in this.expiring so that clearTimeout() |
| // and clearInterval() can detect this situation. |
| |
| if (t.oneshot) { |
| t.removed = true; // flag for removal |
| } else { |
| t.target = now + t.delay; |
| } |
| this.expiring = t; |
| try { |
| t.cb(); |
| } catch (e) { |
| print('timer callback failed, ignored: ' + e); |
| } |
| this.expiring = null; |
| |
| // If the timer was one-shot, it's marked 'removed'. If the user callback |
| // requested deletion for the timer, it's also marked 'removed'. If the |
| // timer is an interval (and is not marked removed), insert it back into |
| // the timer list. |
| |
| if (!t.removed) { |
| // Reinsert interval timer to correct sorted position. The timer |
| // must be an interval timer because one-shot timers are marked |
| // 'removed' above. |
| this.insertTimer(t); |
| } |
| } |
| } |
| |
| EventLoop.run = function() { |
| var wait; |
| var POLLIN = Poll.POLLIN; |
| var POLLOUT = Poll.POLLOUT; |
| var poll_set; |
| var poll_count; |
| var fd; |
| var t, rev; |
| var rc; |
| var acc_res; |
| |
| for (;;) { |
| /* |
| * Process expired timers. |
| */ |
| |
| this.processTimers(); |
| //this.dumpState(); |
| |
| /* |
| * Exit check (may be requested by a user callback) |
| */ |
| |
| if (this.exitRequested) { |
| //print('exit requested, exit'); |
| break; |
| } |
| |
| /* |
| * Create poll socket list. This is a very naive approach. |
| * On Linux, one could use e.g. epoll() and manage socket lists |
| * incrementally. |
| */ |
| |
| poll_set = {}; |
| poll_count = 0; |
| for (fd in this.socketListening) { |
| poll_set[fd] = { events: POLLIN, revents: 0 }; |
| poll_count++; |
| } |
| for (fd in this.socketReading) { |
| poll_set[fd] = { events: POLLIN, revents: 0 }; |
| poll_count++; |
| } |
| for (fd in this.socketConnecting) { |
| poll_set[fd] = { events: POLLOUT, revents: 0 }; |
| poll_count++; |
| } |
| //print(new Date(), 'poll_set IN:', Duktape.enc('jx', poll_set)); |
| |
| /* |
| * Wait timeout for timer closest to expiry. Since the poll |
| * timeout is relative, get this as close to poll() as possible. |
| */ |
| |
| wait = this.getEarliestWait(); |
| if (wait === null) { |
| if (poll_count === 0) { |
| print('no active timers and no sockets to poll, exit'); |
| break; |
| } else { |
| wait = this.maximumWait; |
| } |
| } else { |
| wait = Math.min(this.maximumWait, Math.max(this.minimumWait, wait)); |
| } |
| |
| /* |
| * Do the actual poll. |
| */ |
| |
| try { |
| Poll.poll(poll_set, wait); |
| } catch (e) { |
| // Eat errors silently. When resizing curses window an EINTR |
| // happens now. |
| } |
| |
| /* |
| * Process all sockets so that nothing is left unhandled for the |
| * next round. |
| */ |
| |
| //print(new Date(), 'poll_set OUT:', Duktape.enc('jx', poll_set)); |
| for (fd in poll_set) { |
| t = poll_set[fd]; |
| rev = t.revents; |
| |
| if (rev & POLLIN) { |
| cb = this.socketReading[fd]; |
| if (cb) { |
| data = Socket.read(fd); // no size control now |
| //print('READ', Duktape.enc('jx', data)); |
| if (data.length === 0) { |
| //print('zero read for fd ' + fd + ', closing forcibly'); |
| rc = Socket.close(fd); // ignore result |
| delete this.socketListening[fd]; |
| delete this.socketReading[fd]; |
| } else { |
| cb(fd, data); |
| } |
| } else { |
| cb = this.socketListening[fd]; |
| if (cb) { |
| acc_res = Socket.accept(fd); |
| //print('ACCEPT:', Duktape.enc('jx', acc_res)); |
| cb(acc_res.fd, acc_res.addr, acc_res.port); |
| } else { |
| //print('UNKNOWN'); |
| } |
| } |
| } |
| |
| if (rev & POLLOUT) { |
| cb = this.socketConnecting[fd]; |
| if (cb) { |
| delete this.socketConnecting[fd]; |
| cb(fd); |
| } else { |
| //print('UNKNOWN POLLOUT'); |
| } |
| } |
| |
| if ((rev & ~(POLLIN | POLLOUT)) !== 0) { |
| //print('revents ' + t.revents + ' for fd ' + fd + ', closing forcibly'); |
| rc = Socket.close(fd); // ignore result |
| delete this.socketListening[fd]; |
| delete this.socketReading[fd]; |
| } |
| } |
| } |
| } |
| |
| EventLoop.requestExit = function() { |
| this.exitRequested = true; |
| } |
| |
| EventLoop.server = function(address, port, cb_accepted) { |
| var fd = Socket.createServerSocket(address, port); |
| this.socketListening[fd] = cb_accepted; |
| } |
| |
| EventLoop.connect = function(address, port, cb_connected) { |
| var fd = Socket.connect(address, port); |
| this.socketConnecting[fd] = cb_connected; |
| } |
| |
| EventLoop.close = function(fd) { |
| delete this.socketReading[fd]; |
| delete this.socketListening[fd]; |
| } |
| |
| EventLoop.setReader = function(fd, cb_read) { |
| this.socketReading[fd] = cb_read; |
| } |
| |
| EventLoop.write = function(fd, data) { |
| // This simple example doesn't have support for write blocking / draining |
| var rc = Socket.write(fd, Duktape.Buffer(data)); |
| } |
| |
| /* |
| * Timer API |
| * |
| * These interface with the singleton EventLoop. |
| */ |
| |
| function setTimeout(func, delay) { |
| var cb_func; |
| var bind_args; |
| var timer_id; |
| var evloop = EventLoop; |
| |
| if (typeof delay !== 'number') { |
| throw new TypeError('delay is not a number'); |
| } |
| delay = Math.max(evloop.minimumDelay, delay); |
| |
| if (typeof func === 'string') { |
| // Legacy case: callback is a string. |
| cb_func = eval.bind(this, func); |
| } else if (typeof func !== 'function') { |
| throw new TypeError('callback is not a function/string'); |
| } else if (arguments.length > 2) { |
| // Special case: callback arguments are provided. |
| bind_args = Array.prototype.slice.call(arguments, 2); // [ arg1, arg2, ... ] |
| bind_args.unshift(this); // [ global(this), arg1, arg2, ... ] |
| cb_func = func.bind.apply(func, bind_args); |
| } else { |
| // Normal case: callback given as a function without arguments. |
| cb_func = func; |
| } |
| |
| timer_id = evloop.nextTimerId++; |
| |
| evloop.insertTimer({ |
| id: timer_id, |
| oneshot: true, |
| cb: cb_func, |
| delay: delay, |
| target: Date.now() + delay |
| }); |
| |
| return timer_id; |
| } |
| |
| function clearTimeout(timer_id) { |
| var evloop = EventLoop; |
| |
| if (typeof timer_id !== 'number') { |
| throw new TypeError('timer ID is not a number'); |
| } |
| evloop.removeTimerById(timer_id); |
| } |
| |
| function setInterval(func, delay) { |
| var cb_func; |
| var bind_args; |
| var timer_id; |
| var evloop = EventLoop; |
| |
| if (typeof delay !== 'number') { |
| throw new TypeError('delay is not a number'); |
| } |
| delay = Math.max(evloop.minimumDelay, delay); |
| |
| if (typeof func === 'string') { |
| // Legacy case: callback is a string. |
| cb_func = eval.bind(this, func); |
| } else if (typeof func !== 'function') { |
| throw new TypeError('callback is not a function/string'); |
| } else if (arguments.length > 2) { |
| // Special case: callback arguments are provided. |
| bind_args = Array.prototype.slice.call(arguments, 2); // [ arg1, arg2, ... ] |
| bind_args.unshift(this); // [ global(this), arg1, arg2, ... ] |
| cb_func = func.bind.apply(func, bind_args); |
| } else { |
| // Normal case: callback given as a function without arguments. |
| cb_func = func; |
| } |
| |
| timer_id = evloop.nextTimerId++; |
| |
| evloop.insertTimer({ |
| id: timer_id, |
| oneshot: false, |
| cb: cb_func, |
| delay: delay, |
| target: Date.now() + delay |
| }); |
| |
| return timer_id; |
| } |
| |
| function clearInterval(timer_id) { |
| var evloop = EventLoop; |
| |
| if (typeof timer_id !== 'number') { |
| throw new TypeError('timer ID is not a number'); |
| } |
| evloop.removeTimerById(timer_id); |
| } |
| |
| /* custom call */ |
| function requestEventLoopExit() { |
| EventLoop.requestExit(); |
| } |