+++ /dev/null
-/*
- * 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();
-}