1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | import { ArrayExt, every, retro, some } from '@lumino/algorithm';
|
11 |
|
12 | import { LinkedList } from '@lumino/collections';
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | export class Message {
|
21 | |
22 |
|
23 |
|
24 |
|
25 |
|
26 | constructor(type: string) {
|
27 | this.type = type;
|
28 | }
|
29 |
|
30 | |
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | readonly type: string;
|
39 |
|
40 | |
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | get isConflatable(): boolean {
|
60 | return false;
|
61 | }
|
62 |
|
63 | |
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 | conflate(other: Message): boolean {
|
95 | return false;
|
96 | }
|
97 | }
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 | export class ConflatableMessage extends Message {
|
113 | |
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | get isConflatable(): boolean {
|
120 | return true;
|
121 | }
|
122 |
|
123 | |
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 | conflate(other: ConflatableMessage): boolean {
|
130 | return true;
|
131 | }
|
132 | }
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 | export interface IMessageHandler {
|
145 | |
146 |
|
147 |
|
148 |
|
149 |
|
150 | processMessage(msg: Message): void;
|
151 | }
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 | export interface IMessageHook {
|
170 | |
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 | messageHook(handler: IMessageHandler, msg: Message): boolean;
|
181 | }
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 | export type MessageHook =
|
191 | | IMessageHook
|
192 | | ((handler: IMessageHandler, msg: Message) => boolean);
|
193 |
|
194 | /**
|
195 | * The namespace for the global singleton message loop.
|
196 | */
|
197 | export namespace MessageLoop {
|
198 | /**
|
199 | * A function that cancels the pending loop task; `null` if unavailable.
|
200 | */
|
201 | let pending: (() => void) | null = null;
|
202 |
|
203 | /**
|
204 | * Schedules a function for invocation as soon as possible asynchronously.
|
205 | *
|
206 | * @param fn The function to invoke when called back.
|
207 | *
|
208 | * @returns An anonymous function that will unschedule invocation if possible.
|
209 | */
|
210 | const schedule = (
|
211 | resolved =>
|
212 | (fn: () => unknown): (() => void) => {
|
213 | let rejected = false;
|
214 | resolved.then(() => !rejected && fn());
|
215 | return () => {
|
216 | rejected = true;
|
217 | };
|
218 | }
|
219 | )(Promise.resolve());
|
220 |
|
221 | /**
|
222 | * Send a message to a message handler to process immediately.
|
223 | *
|
224 | * @param handler - The handler which should process the message.
|
225 | *
|
226 | * @param msg - The message to deliver to the handler.
|
227 | *
|
228 | * #### Notes
|
229 | * The message will first be sent through any installed message hooks
|
230 | * for the handler. If the message passes all hooks, it will then be
|
231 | * delivered to the `processMessage` method of the handler.
|
232 | *
|
233 | * The message will not be conflated with pending posted messages.
|
234 | *
|
235 | * Exceptions in hooks and handlers will be caught and logged.
|
236 | */
|
237 | export function sendMessage(handler: IMessageHandler, msg: Message): void {
|
238 | // Lookup the message hooks for the handler.
|
239 | let hooks = messageHooks.get(handler);
|
240 |
|
241 | // Handle the common case of no installed hooks.
|
242 | if (!hooks || hooks.length === 0) {
|
243 | invokeHandler(handler, msg);
|
244 | return;
|
245 | }
|
246 |
|
247 | // Invoke the message hooks starting with the newest first.
|
248 | let passed = every(retro(hooks), hook => {
|
249 | return hook ? invokeHook(hook, handler, msg) : true;
|
250 | });
|
251 |
|
252 | // Invoke the handler if the message passes all hooks.
|
253 | if (passed) {
|
254 | invokeHandler(handler, msg);
|
255 | }
|
256 | }
|
257 |
|
258 | /**
|
259 | * Post a message to a message handler to process in the future.
|
260 | *
|
261 | * @param handler - The handler which should process the message.
|
262 | *
|
263 | * @param msg - The message to post to the handler.
|
264 | *
|
265 | * #### Notes
|
266 | * The message will be conflated with the pending posted messages for
|
267 | * the handler, if possible. If the message is not conflated, it will
|
268 | * be queued for normal delivery on the next cycle of the event loop.
|
269 | *
|
270 | * Exceptions in hooks and handlers will be caught and logged.
|
271 | */
|
272 | export function postMessage(handler: IMessageHandler, msg: Message): void {
|
273 | // Handle the common case of a non-conflatable message.
|
274 | if (!msg.isConflatable) {
|
275 | enqueueMessage(handler, msg);
|
276 | return;
|
277 | }
|
278 |
|
279 | // Conflate the message with an existing message if possible.
|
280 | let conflated = some(messageQueue, posted => {
|
281 | if (posted.handler !== handler) {
|
282 | return false;
|
283 | }
|
284 | if (!posted.msg) {
|
285 | return false;
|
286 | }
|
287 | if (posted.msg.type !== msg.type) {
|
288 | return false;
|
289 | }
|
290 | if (!posted.msg.isConflatable) {
|
291 | return false;
|
292 | }
|
293 | return posted.msg.conflate(msg);
|
294 | });
|
295 |
|
296 | // Enqueue the message if it was not conflated.
|
297 | if (!conflated) {
|
298 | enqueueMessage(handler, msg);
|
299 | }
|
300 | }
|
301 |
|
302 | /**
|
303 | * Install a message hook for a message handler.
|
304 | *
|
305 | * @param handler - The message handler of interest.
|
306 | *
|
307 | * @param hook - The message hook to install.
|
308 | *
|
309 | * #### Notes
|
310 | * A message hook is invoked before a message is delivered to the
|
311 | * handler. If the hook returns `false`, no other hooks will be
|
312 | * invoked and the message will not be delivered to the handler.
|
313 | *
|
314 | * The most recently installed message hook is executed first.
|
315 | *
|
316 | * If the hook is already installed, this is a no-op.
|
317 | */
|
318 | export function installMessageHook(
|
319 | handler: IMessageHandler,
|
320 | hook: MessageHook
|
321 | ): void {
|
322 | // Look up the hooks for the handler.
|
323 | let hooks = messageHooks.get(handler);
|
324 |
|
325 | // Bail early if the hook is already installed.
|
326 | if (hooks && hooks.indexOf(hook) !== -1) {
|
327 | return;
|
328 | }
|
329 |
|
330 | // Add the hook to the end, so it will be the first to execute.
|
331 | if (!hooks) {
|
332 | messageHooks.set(handler, [hook]);
|
333 | } else {
|
334 | hooks.push(hook);
|
335 | }
|
336 | }
|
337 |
|
338 | /**
|
339 | * Remove an installed message hook for a message handler.
|
340 | *
|
341 | * @param handler - The message handler of interest.
|
342 | *
|
343 | * @param hook - The message hook to remove.
|
344 | *
|
345 | * #### Notes
|
346 | * It is safe to call this function while the hook is executing.
|
347 | *
|
348 | * If the hook is not installed, this is a no-op.
|
349 | */
|
350 | export function removeMessageHook(
|
351 | handler: IMessageHandler,
|
352 | hook: MessageHook
|
353 | ): void {
|
354 | // Lookup the hooks for the handler.
|
355 | let hooks = messageHooks.get(handler);
|
356 |
|
357 | // Bail early if the hooks do not exist.
|
358 | if (!hooks) {
|
359 | return;
|
360 | }
|
361 |
|
362 | // Lookup the index of the hook and bail if not found.
|
363 | let i = hooks.indexOf(hook);
|
364 | if (i === -1) {
|
365 | return;
|
366 | }
|
367 |
|
368 | // Clear the hook and schedule a cleanup of the array.
|
369 | hooks[i] = null;
|
370 | scheduleCleanup(hooks);
|
371 | }
|
372 |
|
373 | /**
|
374 | * Clear all message data associated with a message handler.
|
375 | *
|
376 | * @param handler - The message handler of interest.
|
377 | *
|
378 | * #### Notes
|
379 | * This will clear all posted messages and hooks for the handler.
|
380 | */
|
381 | export function clearData(handler: IMessageHandler): void {
|
382 | // Lookup the hooks for the handler.
|
383 | let hooks = messageHooks.get(handler);
|
384 |
|
385 | // Clear all messsage hooks for the handler.
|
386 | if (hooks && hooks.length > 0) {
|
387 | ArrayExt.fill(hooks, null);
|
388 | scheduleCleanup(hooks);
|
389 | }
|
390 |
|
391 | // Clear all posted messages for the handler.
|
392 | for (const posted of messageQueue) {
|
393 | if (posted.handler === handler) {
|
394 | posted.handler = null;
|
395 | posted.msg = null;
|
396 | }
|
397 | }
|
398 | }
|
399 |
|
400 | /**
|
401 | * Process the pending posted messages in the queue immediately.
|
402 | *
|
403 | * #### Notes
|
404 | * This function is useful when posted messages must be processed immediately.
|
405 | *
|
406 | * This function should normally not be needed, but it may be
|
407 | * required to work around certain browser idiosyncrasies.
|
408 | *
|
409 | * Recursing into this function is a no-op.
|
410 | */
|
411 | export function flush(): void {
|
412 | // Bail if recursion is detected or if there is no pending task.
|
413 | if (flushGuard || pending === null) {
|
414 | return;
|
415 | }
|
416 |
|
417 | // Unschedule the pending loop task.
|
418 | pending();
|
419 | pending = null;
|
420 |
|
421 | // Run the message loop within the recursion guard.
|
422 | flushGuard = true;
|
423 | runMessageLoop();
|
424 | flushGuard = false;
|
425 | }
|
426 |
|
427 | /**
|
428 | * A type alias for the exception handler function.
|
429 | */
|
430 | export type ExceptionHandler = (err: Error) => void;
|
431 |
|
432 | |
433 |
|
434 |
|
435 |
|
436 |
|
437 |
|
438 |
|
439 |
|
440 | export function getExceptionHandler(): ExceptionHandler {
|
441 | return exceptionHandler;
|
442 | }
|
443 |
|
444 | |
445 |
|
446 |
|
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 | export function setExceptionHandler(
|
456 | handler: ExceptionHandler
|
457 | ): ExceptionHandler {
|
458 | let old = exceptionHandler;
|
459 | exceptionHandler = handler;
|
460 | return old;
|
461 | }
|
462 |
|
463 | |
464 |
|
465 |
|
466 | type PostedMessage = { handler: IMessageHandler | null; msg: Message | null };
|
467 |
|
468 | |
469 |
|
470 |
|
471 | const messageQueue = new LinkedList<PostedMessage>();
|
472 |
|
473 | |
474 |
|
475 |
|
476 | const messageHooks = new WeakMap<
|
477 | IMessageHandler,
|
478 | Array<MessageHook | null>
|
479 | >();
|
480 |
|
481 | |
482 |
|
483 |
|
484 | const dirtySet = new Set<Array<MessageHook | null>>();
|
485 |
|
486 | |
487 |
|
488 |
|
489 | let exceptionHandler: ExceptionHandler = (err: Error) => {
|
490 | console.error(err);
|
491 | };
|
492 |
|
493 | |
494 |
|
495 |
|
496 | let flushGuard = false;
|
497 |
|
498 | |
499 |
|
500 |
|
501 |
|
502 |
|
503 |
|
504 |
|
505 | function invokeHook(
|
506 | hook: MessageHook,
|
507 | handler: IMessageHandler,
|
508 | msg: Message
|
509 | ): boolean {
|
510 | let result = true;
|
511 | try {
|
512 | if (typeof hook === 'function') {
|
513 | result = hook(handler, msg);
|
514 | } else {
|
515 | result = hook.messageHook(handler, msg);
|
516 | }
|
517 | } catch (err) {
|
518 | exceptionHandler(err);
|
519 | }
|
520 | return result;
|
521 | }
|
522 |
|
523 | |
524 |
|
525 |
|
526 |
|
527 |
|
528 | function invokeHandler(handler: IMessageHandler, msg: Message): void {
|
529 | try {
|
530 | handler.processMessage(msg);
|
531 | } catch (err) {
|
532 | exceptionHandler(err);
|
533 | }
|
534 | }
|
535 |
|
536 | |
537 |
|
538 |
|
539 |
|
540 |
|
541 | function enqueueMessage(handler: IMessageHandler, msg: Message): void {
|
542 |
|
543 | messageQueue.addLast({ handler, msg });
|
544 |
|
545 |
|
546 | if (pending !== null) {
|
547 | return;
|
548 | }
|
549 |
|
550 |
|
551 | pending = schedule(runMessageLoop);
|
552 | }
|
553 |
|
554 | |
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 | function runMessageLoop(): void {
|
562 |
|
563 | pending = null;
|
564 |
|
565 |
|
566 | if (messageQueue.isEmpty) {
|
567 | return;
|
568 | }
|
569 |
|
570 |
|
571 |
|
572 |
|
573 | let sentinel: PostedMessage = { handler: null, msg: null };
|
574 | messageQueue.addLast(sentinel);
|
575 |
|
576 |
|
577 |
|
578 | while (true) {
|
579 |
|
580 | let posted = messageQueue.removeFirst()!;
|
581 |
|
582 |
|
583 | if (posted === sentinel) {
|
584 | return;
|
585 | }
|
586 |
|
587 |
|
588 | if (posted.handler && posted.msg) {
|
589 | sendMessage(posted.handler, posted.msg);
|
590 | }
|
591 | }
|
592 | }
|
593 |
|
594 | |
595 |
|
596 |
|
597 |
|
598 |
|
599 |
|
600 |
|
601 | function scheduleCleanup(hooks: Array<MessageHook | null>): void {
|
602 | if (dirtySet.size === 0) {
|
603 | schedule(cleanupDirtySet);
|
604 | }
|
605 | dirtySet.add(hooks);
|
606 | }
|
607 |
|
608 | |
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 | function cleanupDirtySet(): void {
|
615 | dirtySet.forEach(cleanupHooks);
|
616 | dirtySet.clear();
|
617 | }
|
618 |
|
619 | |
620 |
|
621 |
|
622 |
|
623 |
|
624 |
|
625 |
|
626 |
|
627 | function cleanupHooks(hooks: Array<MessageHook | null>): void {
|
628 | ArrayExt.removeAllWhere(hooks, isNull);
|
629 | }
|
630 |
|
631 | |
632 |
|
633 |
|
634 | function isNull<T>(value: T | null): boolean {
|
635 | return value === null;
|
636 | }
|
637 | }
|