]> git.proxmox.com Git - ceph.git/blob - ceph/src/civetweb/src/third_party/duktape-1.3.0/examples/eventloop/ecma_eventloop.js
add subtree-ish sources for 12.0.3
[ceph.git] / ceph / src / civetweb / src / third_party / duktape-1.3.0 / examples / eventloop / ecma_eventloop.js
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
22 EventLoop = {
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
41 EventLoop.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.
53 EventLoop.getEarliestTimer = function() {
54 var timers = this.timers;
55 n = timers.length;
56 return (n > 0 ? timers[n - 1] : null);
57 }
58
59 EventLoop.getEarliestWait = function() {
60 var t = this.getEarliestTimer();
61 return (t ? t.target - Date.now() : null);
62 }
63
64 EventLoop.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.
88 EventLoop.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
120 EventLoop.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
200 EventLoop.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
333 EventLoop.requestExit = function() {
334 this.exitRequested = true;
335 }
336
337 EventLoop.server = function(address, port, cb_accepted) {
338 var fd = Socket.createServerSocket(address, port);
339 this.socketListening[fd] = cb_accepted;
340 }
341
342 EventLoop.connect = function(address, port, cb_connected) {
343 var fd = Socket.connect(address, port);
344 this.socketConnecting[fd] = cb_connected;
345 }
346
347 EventLoop.close = function(fd) {
348 delete this.socketReading[fd];
349 delete this.socketListening[fd];
350 }
351
352 EventLoop.setReader = function(fd, cb_read) {
353 this.socketReading[fd] = cb_read;
354 }
355
356 EventLoop.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
367 function 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
406 function 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
415 function 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
454 function 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 */
464 function requestEventLoopExit() {
465 EventLoop.requestExit();
466 }