1/*
2 *  Pure Ecmascript eventloop example.
3 *
4 *  Timer state handling is inefficient in this trivial example.  Timers are
5 *  kept in an array sorted by their expiry time which works well for expiring
6 *  timers, but has O(n) insertion performance.  A better implementation would
7 *  use a heap or some other efficient structure for managing timers so that
8 *  all operations (insert, remove, get nearest timer) have good performance.
9 *
10 *  https://developer.mozilla.org/en-US/docs/Web/JavaScript/Timers
11 */
12
13/*
14 *  Event loop
15 *
16 *  Timers are sorted by 'target' property which indicates expiry time of
17 *  the timer.  The timer expiring next is last in the array, so that
18 *  removals happen at the end, and inserts for timers expiring in the
19 *  near future displace as few elements in the array as possible.
20 */
21
22EventLoop = {
23    // timers
24    timers: [],         // active timers, sorted (nearest expiry last)
25    expiring: null,     // set to timer being expired (needs special handling in clearTimeout/clearInterval)
26    nextTimerId: 1,
27    minimumDelay: 1,
28    minimumWait: 1,
29    maximumWait: 60000,
30    maxExpirys: 10,
31
32    // sockets
33    socketListening: {},  // fd -> callback
34    socketReading: {},    // fd -> callback
35    socketConnecting: {}, // fd -> callback
36
37    // misc
38    exitRequested: false
39};
40
41EventLoop.dumpState = function() {
42    print('TIMER STATE:');
43    this.timers.forEach(function(t) {
44        print('    ' + Duktape.enc('jx', t));
45    });
46    if (this.expiring) {
47        print('    EXPIRING: ' + Duktape.enc('jx', this.expiring));
48    }
49}
50
51// Get timer with lowest expiry time.  Since the active timers list is
52// sorted, it's always the last timer.
53EventLoop.getEarliestTimer = function() {
54    var timers = this.timers;
55    n = timers.length;
56    return (n > 0 ? timers[n - 1] : null);
57}
58
59EventLoop.getEarliestWait = function() {
60    var t = this.getEarliestTimer();
61    return (t ? t.target - Date.now() : null);
62}
63
64EventLoop.insertTimer = function(timer) {
65    var timers = this.timers;
66    var i, n, t;
67
68    /*
69     *  Find 'i' such that we want to insert *after* timers[i] at index i+1.
70     *  If no such timer, for-loop terminates with i-1, and we insert at -1+1=0.
71     */
72
73    n = timers.length;
74    for (i = n - 1; i >= 0; i--) {
75        t = timers[i];
76        if (timer.target <= t.target) {
77            // insert after 't', to index i+1
78            break;
79        }
80    }
81
82    timers.splice(i + 1 /*start*/, 0 /*deleteCount*/, timer);
83}
84
85// Remove timer/interval with a timer ID.  The timer/interval can reside
86// either on the active list or it may be an expired timer (this.expiring)
87// whose user callback we're running when this function gets called.
88EventLoop.removeTimerById = function(timer_id) {
89    var timers = this.timers;
90    var i, n, t;
91
92    t = this.expiring;
93    if (t) {
94        if (t.id === timer_id) {
95            // Timer has expired and we're processing its callback.  User
96            // callback has requested timer deletion.  Mark removed, so
97            // that the timer is not reinserted back into the active list.
98            // This is actually a common case because an interval may very
99            // well cancel itself.
100            t.removed = true;
101            return;
102        }
103    }
104
105    n = timers.length;
106    for (i = 0; i < n; i++) {
107        t = timers[i];
108        if (t.id === timer_id) {
109            // Timer on active list: mark removed (not really necessary, but
110            // nice for dumping), and remove from active list.
111            t.removed = true;
112            this.timers.splice(i /*start*/, 1 /*deleteCount*/);
113            return;
114        }
115    }
116
117   // no such ID, ignore
118}
119
120EventLoop.processTimers = function() {
121    var now = Date.now();
122    var timers = this.timers;
123    var sanity = this.maxExpirys;
124    var n, t;
125
126    /*
127     *  Here we must be careful with mutations: user callback may add and
128     *  delete an arbitrary number of timers.
129     *
130     *  Current solution is simple: check whether the timer at the end of
131     *  the list has expired.  If not, we're done.  If it has expired,
132     *  remove it from the active list, record it in this.expiring, and call
133     *  the user callback.  If user code deletes the this.expiring timer,
134     *  there is special handling which just marks the timer deleted so
135     *  it won't get inserted back into the active list.
136     *
137     *  This process is repeated at most maxExpirys times to ensure we don't
138     *  get stuck forever; user code could in principle add more and more
139     *  already expired timers.
140     */
141
142    while (sanity-- > 0) {
143        // If exit requested, don't call any more callbacks.  This allows
144        // a callback to do cleanups and request exit, and can be sure that
145        // no more callbacks are processed.
146
147        if (this.exitRequested) {
148            //print('exit requested, exit');
149            break;
150        }
151
152        // Timers to expire?
153
154        n = timers.length;
155        if (n <= 0) {
156            break;
157        }
158        t = timers[n - 1];
159        if (now <= t.target) {
160            // Timer has not expired, and no other timer could have expired
161            // either because the list is sorted.
162            break;
163        }
164        timers.pop();
165
166        // Remove the timer from the active list and process it.  The user
167        // callback may add new timers which is not a problem.  The callback
168        // may also delete timers which is not a problem unless the timer
169        // being deleted is the timer whose callback we're running; this is
170        // why the timer is recorded in this.expiring so that clearTimeout()
171        // and clearInterval() can detect this situation.
172
173        if (t.oneshot) {
174            t.removed = true;  // flag for removal
175        } else {
176            t.target = now + t.delay;
177        }
178        this.expiring = t;
179        try {
180            t.cb();
181        } catch (e) {
182            print('timer callback failed, ignored: ' + e);
183        }
184        this.expiring = null;
185
186        // If the timer was one-shot, it's marked 'removed'.  If the user callback
187        // requested deletion for the timer, it's also marked 'removed'.  If the
188        // timer is an interval (and is not marked removed), insert it back into
189        // the timer list.
190
191        if (!t.removed) {
192            // Reinsert interval timer to correct sorted position.  The timer
193            // must be an interval timer because one-shot timers are marked
194            // 'removed' above.
195            this.insertTimer(t);
196        }
197    }
198}
199
200EventLoop.run = function() {
201    var wait;
202    var POLLIN = Poll.POLLIN;
203    var POLLOUT = Poll.POLLOUT;
204    var poll_set;
205    var poll_count;
206    var fd;
207    var t, rev;
208    var rc;
209    var acc_res;
210
211    for (;;) {
212        /*
213         *  Process expired timers.
214         */
215
216        this.processTimers();
217        //this.dumpState();
218
219        /*
220         *  Exit check (may be requested by a user callback)
221         */
222
223        if (this.exitRequested) {
224            //print('exit requested, exit');
225            break;
226        }
227
228        /*
229         *  Create poll socket list.  This is a very naive approach.
230         *  On Linux, one could use e.g. epoll() and manage socket lists
231         *  incrementally.
232         */
233
234        poll_set = {};
235        poll_count = 0;
236        for (fd in this.socketListening) {
237            poll_set[fd] = { events: POLLIN, revents: 0 };
238            poll_count++;
239        }
240        for (fd in this.socketReading) {
241            poll_set[fd] = { events: POLLIN, revents: 0 };
242            poll_count++;
243        }
244        for (fd in this.socketConnecting) {
245            poll_set[fd] = { events: POLLOUT, revents: 0 };
246            poll_count++;
247        }
248        //print(new Date(), 'poll_set IN:', Duktape.enc('jx', poll_set));
249
250        /*
251         *  Wait timeout for timer closest to expiry.  Since the poll
252         *  timeout is relative, get this as close to poll() as possible.
253         */
254
255        wait = this.getEarliestWait();
256        if (wait === null) {
257            if (poll_count === 0) {
258                print('no active timers and no sockets to poll, exit');
259                break;
260            } else {
261                wait = this.maximumWait;
262            }
263        } else {
264            wait = Math.min(this.maximumWait, Math.max(this.minimumWait, wait));
265        }
266
267        /*
268         *  Do the actual poll.
269         */
270
271        try {
272            Poll.poll(poll_set, wait);
273        } catch (e) {
274            // Eat errors silently.  When resizing curses window an EINTR
275            // happens now.
276        }
277
278        /*
279         *  Process all sockets so that nothing is left unhandled for the
280         *  next round.
281         */
282
283        //print(new Date(), 'poll_set OUT:', Duktape.enc('jx', poll_set));
284        for (fd in poll_set) {
285            t = poll_set[fd];
286            rev = t.revents;
287
288            if (rev & POLLIN) {
289                cb = this.socketReading[fd];
290                if (cb) {
291                    data = Socket.read(fd);  // no size control now
292                    //print('READ', Duktape.enc('jx', data));
293                    if (data.length === 0) {
294                        //print('zero read for fd ' + fd + ', closing forcibly');
295                        rc = Socket.close(fd);  // ignore result
296                        delete this.socketListening[fd];
297                        delete this.socketReading[fd];
298                    } else {
299                        cb(fd, data);
300                    }
301                } else {
302                    cb = this.socketListening[fd];
303                    if (cb) {
304                        acc_res = Socket.accept(fd);
305                        //print('ACCEPT:', Duktape.enc('jx', acc_res));
306                        cb(acc_res.fd, acc_res.addr, acc_res.port);
307                    } else {
308                        //print('UNKNOWN');
309                    }
310                }
311            }
312
313            if (rev & POLLOUT) {
314                cb = this.socketConnecting[fd];
315                if (cb) {
316                    delete this.socketConnecting[fd];
317                    cb(fd);
318                } else {
319                    //print('UNKNOWN POLLOUT');
320                }
321            }
322
323            if ((rev & ~(POLLIN | POLLOUT)) !== 0) {
324                //print('revents ' + t.revents + ' for fd ' + fd + ', closing forcibly');
325                rc = Socket.close(fd);  // ignore result
326                delete this.socketListening[fd];
327                delete this.socketReading[fd];
328            }
329        }
330    }
331}
332
333EventLoop.requestExit = function() {
334    this.exitRequested = true;
335}
336
337EventLoop.server = function(address, port, cb_accepted) {
338    var fd = Socket.createServerSocket(address, port);
339    this.socketListening[fd] = cb_accepted;
340}
341
342EventLoop.connect = function(address, port, cb_connected) {
343    var fd = Socket.connect(address, port);
344    this.socketConnecting[fd] = cb_connected;
345}
346
347EventLoop.close = function(fd) {
348    delete this.socketReading[fd];
349    delete this.socketListening[fd];
350}
351
352EventLoop.setReader = function(fd, cb_read) {
353    this.socketReading[fd] = cb_read;
354}
355
356EventLoop.write = function(fd, data) {
357    // This simple example doesn't have support for write blocking / draining
358    var rc = Socket.write(fd, Duktape.Buffer(data));
359}
360
361/*
362 *  Timer API
363 *
364 *  These interface with the singleton EventLoop.
365 */
366
367function setTimeout(func, delay) {
368    var cb_func;
369    var bind_args;
370    var timer_id;
371    var evloop = EventLoop;
372
373    if (typeof delay !== 'number') {
374        throw new TypeError('delay is not a number');
375    }
376    delay = Math.max(evloop.minimumDelay, delay);
377
378    if (typeof func === 'string') {
379        // Legacy case: callback is a string.
380        cb_func = eval.bind(this, func);
381    } else if (typeof func !== 'function') {
382        throw new TypeError('callback is not a function/string');
383    } else if (arguments.length > 2) {
384        // Special case: callback arguments are provided.
385        bind_args = Array.prototype.slice.call(arguments, 2);  // [ arg1, arg2, ... ]
386        bind_args.unshift(this);  // [ global(this), arg1, arg2, ... ]
387        cb_func = func.bind.apply(func, bind_args);
388    } else {
389        // Normal case: callback given as a function without arguments.
390        cb_func = func;
391    }
392
393    timer_id = evloop.nextTimerId++;
394
395    evloop.insertTimer({
396        id: timer_id,
397        oneshot: true,
398        cb: cb_func,
399        delay: delay,
400        target: Date.now() + delay
401    });
402
403    return timer_id;
404}
405
406function clearTimeout(timer_id) {
407    var evloop = EventLoop;
408
409    if (typeof timer_id !== 'number') {
410        throw new TypeError('timer ID is not a number');
411    }
412    evloop.removeTimerById(timer_id);
413}
414
415function setInterval(func, delay) {
416    var cb_func;
417    var bind_args;
418    var timer_id;
419    var evloop = EventLoop;
420
421    if (typeof delay !== 'number') {
422        throw new TypeError('delay is not a number');
423    }
424    delay = Math.max(evloop.minimumDelay, delay);
425
426    if (typeof func === 'string') {
427        // Legacy case: callback is a string.
428        cb_func = eval.bind(this, func);
429    } else if (typeof func !== 'function') {
430        throw new TypeError('callback is not a function/string');
431    } else if (arguments.length > 2) {
432        // Special case: callback arguments are provided.
433        bind_args = Array.prototype.slice.call(arguments, 2);  // [ arg1, arg2, ... ]
434        bind_args.unshift(this);  // [ global(this), arg1, arg2, ... ]
435        cb_func = func.bind.apply(func, bind_args);
436    } else {
437        // Normal case: callback given as a function without arguments.
438        cb_func = func;
439    }
440
441    timer_id = evloop.nextTimerId++;
442
443    evloop.insertTimer({
444        id: timer_id,
445        oneshot: false,
446        cb: cb_func,
447        delay: delay,
448        target: Date.now() + delay
449    });
450
451    return timer_id;
452}
453
454function clearInterval(timer_id) {
455    var evloop = EventLoop;
456
457    if (typeof timer_id !== 'number') {
458        throw new TypeError('timer ID is not a number');
459    }
460    evloop.removeTimerById(timer_id);
461}
462
463/* custom call */
464function requestEventLoopExit() {
465    EventLoop.requestExit();
466}
467