UNPKG

18.1 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3/*-----------------------------------------------------------------------------
4| Copyright (c) 2014-2017, PhosphorJS Contributors
5|
6| Distributed under the terms of the BSD 3-Clause License.
7|
8| The full license is in the file LICENSE, distributed with this software.
9|----------------------------------------------------------------------------*/
10import { ArrayExt, every, retro, some } from '@lumino/algorithm';
11
12import { LinkedList } from '@lumino/collections';
13
14/**
15 * A message which can be delivered to a message handler.
16 *
17 * #### Notes
18 * This class may be subclassed to create complex message types.
19 */
20export class Message {
21 /**
22 * Construct a new message.
23 *
24 * @param type - The type of the message.
25 */
26 constructor(type: string) {
27 this.type = type;
28 }
29
30 /**
31 * The type of the message.
32 *
33 * #### Notes
34 * The `type` of a message should be related directly to its actual
35 * runtime type. This means that `type` can and will be used to cast
36 * the message to the relevant derived `Message` subtype.
37 */
38 readonly type: string;
39
40 /**
41 * Test whether the message is conflatable.
42 *
43 * #### Notes
44 * Message conflation is an advanced topic. Most message types will
45 * not make use of this feature.
46 *
47 * If a conflatable message is posted to a handler while another
48 * conflatable message of the same `type` has already been posted
49 * to the handler, the `conflate()` method of the existing message
50 * will be invoked. If that method returns `true`, the new message
51 * will not be enqueued. This allows messages to be compressed, so
52 * that only a single instance of the message type is processed per
53 * cycle, no matter how many times messages of that type are posted.
54 *
55 * Custom message types may reimplement this property.
56 *
57 * The default implementation is always `false`.
58 */
59 get isConflatable(): boolean {
60 return false;
61 }
62
63 /**
64 * Conflate this message with another message of the same `type`.
65 *
66 * @param other - A conflatable message of the same `type`.
67 *
68 * @returns `true` if the message was successfully conflated, or
69 * `false` otherwise.
70 *
71 * #### Notes
72 * Message conflation is an advanced topic. Most message types will
73 * not make use of this feature.
74 *
75 * This method is called automatically by the message loop when the
76 * given message is posted to the handler paired with this message.
77 * This message will already be enqueued and conflatable, and the
78 * given message will have the same `type` and also be conflatable.
79 *
80 * This method should merge the state of the other message into this
81 * message as needed so that when this message is finally delivered
82 * to the handler, it receives the most up-to-date information.
83 *
84 * If this method returns `true`, it signals that the other message
85 * was successfully conflated and that message will not be enqueued.
86 *
87 * If this method returns `false`, the other message will be enqueued
88 * for normal delivery.
89 *
90 * Custom message types may reimplement this method.
91 *
92 * The default implementation always returns `false`.
93 */
94 conflate(other: Message): boolean {
95 return false;
96 }
97}
98
99/**
100 * A convenience message class which conflates automatically.
101 *
102 * #### Notes
103 * Message conflation is an advanced topic. Most user code will not
104 * make use of this class.
105 *
106 * This message class is useful for creating message instances which
107 * should be conflated, but which have no state other than `type`.
108 *
109 * If conflation of stateful messages is required, a custom `Message`
110 * subclass should be created.
111 */
112export class ConflatableMessage extends Message {
113 /**
114 * Test whether the message is conflatable.
115 *
116 * #### Notes
117 * This property is always `true`.
118 */
119 get isConflatable(): boolean {
120 return true;
121 }
122
123 /**
124 * Conflate this message with another message of the same `type`.
125 *
126 * #### Notes
127 * This method always returns `true`.
128 */
129 conflate(other: ConflatableMessage): boolean {
130 return true;
131 }
132}
133
134/**
135 * An object which handles messages.
136 *
137 * #### Notes
138 * A message handler is a simple way of defining a type which can act
139 * upon on a large variety of external input without requiring a large
140 * abstract API surface. This is particularly useful in the context of
141 * widget frameworks where the number of distinct message types can be
142 * unbounded.
143 */
144export interface IMessageHandler {
145 /**
146 * Process a message sent to the handler.
147 *
148 * @param msg - The message to be processed.
149 */
150 processMessage(msg: Message): void;
151}
152
153/**
154 * An object which intercepts messages sent to a message handler.
155 *
156 * #### Notes
157 * A message hook is useful for intercepting or spying on messages
158 * sent to message handlers which were either not created by the
159 * consumer, or when subclassing the handler is not feasible.
160 *
161 * If `messageHook` returns `false`, no other message hooks will be
162 * invoked and the message will not be delivered to the handler.
163 *
164 * If all installed message hooks return `true`, the message will
165 * be delivered to the handler for processing.
166 *
167 * **See also:** {@link MessageLoop.installMessageHook} and {@link MessageLoop.removeMessageHook}
168 */
169export interface IMessageHook {
170 /**
171 * Intercept a message sent to a message handler.
172 *
173 * @param handler - The target handler of the message.
174 *
175 * @param msg - The message to be sent to the handler.
176 *
177 * @returns `true` if the message should continue to be processed
178 * as normal, or `false` if processing should cease immediately.
179 */
180 messageHook(handler: IMessageHandler, msg: Message): boolean;
181}
182
183/**
184 * A type alias for message hook object or function.
185 *
186 * #### Notes
187 * The signature and semantics of a message hook function are the same
188 * as the `messageHook` method of {@link IMessageHook}.
189 */
190export type MessageHook =
191 | IMessageHook
192 | ((handler: IMessageHandler, msg: Message) => boolean);
193
194/**
195 * The namespace for the global singleton message loop.
196 */
197export 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 * Get the message loop exception handler.
434 *
435 * @returns The current exception handler.
436 *
437 * #### Notes
438 * The default exception handler is `console.error`.
439 */
440 export function getExceptionHandler(): ExceptionHandler {
441 return exceptionHandler;
442 }
443
444 /**
445 * Set the message loop exception handler.
446 *
447 * @param handler - The function to use as the exception handler.
448 *
449 * @returns The old exception handler.
450 *
451 * #### Notes
452 * The exception handler is invoked when a message handler or a
453 * message hook throws an exception.
454 */
455 export function setExceptionHandler(
456 handler: ExceptionHandler
457 ): ExceptionHandler {
458 let old = exceptionHandler;
459 exceptionHandler = handler;
460 return old;
461 }
462
463 /**
464 * A type alias for a posted message pair.
465 */
466 type PostedMessage = { handler: IMessageHandler | null; msg: Message | null };
467
468 /**
469 * The queue of posted message pairs.
470 */
471 const messageQueue = new LinkedList<PostedMessage>();
472
473 /**
474 * A mapping of handler to array of installed message hooks.
475 */
476 const messageHooks = new WeakMap<
477 IMessageHandler,
478 Array<MessageHook | null>
479 >();
480
481 /**
482 * A set of message hook arrays which are pending cleanup.
483 */
484 const dirtySet = new Set<Array<MessageHook | null>>();
485
486 /**
487 * The message loop exception handler.
488 */
489 let exceptionHandler: ExceptionHandler = (err: Error) => {
490 console.error(err);
491 };
492
493 /**
494 * A guard flag to prevent flush recursion.
495 */
496 let flushGuard = false;
497
498 /**
499 * Invoke a message hook with the specified handler and message.
500 *
501 * Returns the result of the hook, or `true` if the hook throws.
502 *
503 * Exceptions in the hook will be caught and logged.
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 * Invoke a message handler with the specified message.
525 *
526 * Exceptions in the handler will be caught and logged.
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 * Add a message to the end of the message queue.
538 *
539 * This will automatically schedule a run of the message loop.
540 */
541 function enqueueMessage(handler: IMessageHandler, msg: Message): void {
542 // Add the posted message to the queue.
543 messageQueue.addLast({ handler, msg });
544
545 // Bail if a loop task is already pending.
546 if (pending !== null) {
547 return;
548 }
549
550 // Schedule a run of the message loop.
551 pending = schedule(runMessageLoop);
552 }
553
554 /**
555 * Run an iteration of the message loop.
556 *
557 * This will process all pending messages in the queue. If a message
558 * is added to the queue while the message loop is running, it will
559 * be processed on the next cycle of the loop.
560 */
561 function runMessageLoop(): void {
562 // Clear the task so the next loop can be scheduled.
563 pending = null;
564
565 // If the message queue is empty, there is nothing else to do.
566 if (messageQueue.isEmpty) {
567 return;
568 }
569
570 // Add a sentinel value to the end of the queue. The queue will
571 // only be processed up to the sentinel. Messages posted during
572 // this cycle will execute on the next cycle.
573 let sentinel: PostedMessage = { handler: null, msg: null };
574 messageQueue.addLast(sentinel);
575
576 // Enter the message loop.
577 // eslint-disable-next-line no-constant-condition
578 while (true) {
579 // Remove the first posted message in the queue.
580 let posted = messageQueue.removeFirst()!;
581
582 // If the value is the sentinel, exit the loop.
583 if (posted === sentinel) {
584 return;
585 }
586
587 // Dispatch the message if it has not been cleared.
588 if (posted.handler && posted.msg) {
589 sendMessage(posted.handler, posted.msg);
590 }
591 }
592 }
593
594 /**
595 * Schedule a cleanup of a message hooks array.
596 *
597 * This will add the array to the dirty set and schedule a deferred
598 * cleanup of the array contents. On cleanup, any `null` hook will
599 * be removed from the array.
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 * Cleanup the message hook arrays in the dirty set.
610 *
611 * This function should only be invoked asynchronously, when the
612 * stack frame is guaranteed to not be on the path of user code.
613 */
614 function cleanupDirtySet(): void {
615 dirtySet.forEach(cleanupHooks);
616 dirtySet.clear();
617 }
618
619 /**
620 * Cleanup the dirty hooks in a message hooks array.
621 *
622 * This will remove any `null` hook from the array.
623 *
624 * This function should only be invoked asynchronously, when the
625 * stack frame is guaranteed to not be on the path of user code.
626 */
627 function cleanupHooks(hooks: Array<MessageHook | null>): void {
628 ArrayExt.removeAllWhere(hooks, isNull);
629 }
630
631 /**
632 * Test whether a value is `null`.
633 */
634 function isNull<T>(value: T | null): boolean {
635 return value === null;
636 }
637}