UNPKG

18.8 kBJavaScriptView Raw
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@lumino/algorithm'), require('@lumino/collections')) :
3 typeof define === 'function' && define.amd ? define(['exports', '@lumino/algorithm', '@lumino/collections'], factory) :
4 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.lumino_messaging = {}, global.lumino_algorithm, global.lumino_collections));
5})(this, (function (exports, algorithm, collections) { 'use strict';
6
7 // Copyright (c) Jupyter Development Team.
8 /**
9 * A message which can be delivered to a message handler.
10 *
11 * #### Notes
12 * This class may be subclassed to create complex message types.
13 */
14 class Message {
15 /**
16 * Construct a new message.
17 *
18 * @param type - The type of the message.
19 */
20 constructor(type) {
21 this.type = type;
22 }
23 /**
24 * Test whether the message is conflatable.
25 *
26 * #### Notes
27 * Message conflation is an advanced topic. Most message types will
28 * not make use of this feature.
29 *
30 * If a conflatable message is posted to a handler while another
31 * conflatable message of the same `type` has already been posted
32 * to the handler, the `conflate()` method of the existing message
33 * will be invoked. If that method returns `true`, the new message
34 * will not be enqueued. This allows messages to be compressed, so
35 * that only a single instance of the message type is processed per
36 * cycle, no matter how many times messages of that type are posted.
37 *
38 * Custom message types may reimplement this property.
39 *
40 * The default implementation is always `false`.
41 */
42 get isConflatable() {
43 return false;
44 }
45 /**
46 * Conflate this message with another message of the same `type`.
47 *
48 * @param other - A conflatable message of the same `type`.
49 *
50 * @returns `true` if the message was successfully conflated, or
51 * `false` otherwise.
52 *
53 * #### Notes
54 * Message conflation is an advanced topic. Most message types will
55 * not make use of this feature.
56 *
57 * This method is called automatically by the message loop when the
58 * given message is posted to the handler paired with this message.
59 * This message will already be enqueued and conflatable, and the
60 * given message will have the same `type` and also be conflatable.
61 *
62 * This method should merge the state of the other message into this
63 * message as needed so that when this message is finally delivered
64 * to the handler, it receives the most up-to-date information.
65 *
66 * If this method returns `true`, it signals that the other message
67 * was successfully conflated and that message will not be enqueued.
68 *
69 * If this method returns `false`, the other message will be enqueued
70 * for normal delivery.
71 *
72 * Custom message types may reimplement this method.
73 *
74 * The default implementation always returns `false`.
75 */
76 conflate(other) {
77 return false;
78 }
79 }
80 /**
81 * A convenience message class which conflates automatically.
82 *
83 * #### Notes
84 * Message conflation is an advanced topic. Most user code will not
85 * make use of this class.
86 *
87 * This message class is useful for creating message instances which
88 * should be conflated, but which have no state other than `type`.
89 *
90 * If conflation of stateful messages is required, a custom `Message`
91 * subclass should be created.
92 */
93 class ConflatableMessage extends Message {
94 /**
95 * Test whether the message is conflatable.
96 *
97 * #### Notes
98 * This property is always `true`.
99 */
100 get isConflatable() {
101 return true;
102 }
103 /**
104 * Conflate this message with another message of the same `type`.
105 *
106 * #### Notes
107 * This method always returns `true`.
108 */
109 conflate(other) {
110 return true;
111 }
112 }
113 /**
114 * The namespace for the global singleton message loop.
115 */
116 exports.MessageLoop = void 0;
117 (function (MessageLoop) {
118 /**
119 * A function that cancels the pending loop task; `null` if unavailable.
120 */
121 let pending = null;
122 /**
123 * Schedules a function for invocation as soon as possible asynchronously.
124 *
125 * @param fn The function to invoke when called back.
126 *
127 * @returns An anonymous function that will unschedule invocation if possible.
128 */
129 const schedule = (resolved => (fn) => {
130 let rejected = false;
131 resolved.then(() => !rejected && fn());
132 return () => {
133 rejected = true;
134 };
135 })(Promise.resolve());
136 /**
137 * Send a message to a message handler to process immediately.
138 *
139 * @param handler - The handler which should process the message.
140 *
141 * @param msg - The message to deliver to the handler.
142 *
143 * #### Notes
144 * The message will first be sent through any installed message hooks
145 * for the handler. If the message passes all hooks, it will then be
146 * delivered to the `processMessage` method of the handler.
147 *
148 * The message will not be conflated with pending posted messages.
149 *
150 * Exceptions in hooks and handlers will be caught and logged.
151 */
152 function sendMessage(handler, msg) {
153 // Lookup the message hooks for the handler.
154 let hooks = messageHooks.get(handler);
155 // Handle the common case of no installed hooks.
156 if (!hooks || hooks.length === 0) {
157 invokeHandler(handler, msg);
158 return;
159 }
160 // Invoke the message hooks starting with the newest first.
161 let passed = algorithm.every(algorithm.retro(hooks), hook => {
162 return hook ? invokeHook(hook, handler, msg) : true;
163 });
164 // Invoke the handler if the message passes all hooks.
165 if (passed) {
166 invokeHandler(handler, msg);
167 }
168 }
169 MessageLoop.sendMessage = sendMessage;
170 /**
171 * Post a message to a message handler to process in the future.
172 *
173 * @param handler - The handler which should process the message.
174 *
175 * @param msg - The message to post to the handler.
176 *
177 * #### Notes
178 * The message will be conflated with the pending posted messages for
179 * the handler, if possible. If the message is not conflated, it will
180 * be queued for normal delivery on the next cycle of the event loop.
181 *
182 * Exceptions in hooks and handlers will be caught and logged.
183 */
184 function postMessage(handler, msg) {
185 // Handle the common case of a non-conflatable message.
186 if (!msg.isConflatable) {
187 enqueueMessage(handler, msg);
188 return;
189 }
190 // Conflate the message with an existing message if possible.
191 let conflated = algorithm.some(messageQueue, posted => {
192 if (posted.handler !== handler) {
193 return false;
194 }
195 if (!posted.msg) {
196 return false;
197 }
198 if (posted.msg.type !== msg.type) {
199 return false;
200 }
201 if (!posted.msg.isConflatable) {
202 return false;
203 }
204 return posted.msg.conflate(msg);
205 });
206 // Enqueue the message if it was not conflated.
207 if (!conflated) {
208 enqueueMessage(handler, msg);
209 }
210 }
211 MessageLoop.postMessage = postMessage;
212 /**
213 * Install a message hook for a message handler.
214 *
215 * @param handler - The message handler of interest.
216 *
217 * @param hook - The message hook to install.
218 *
219 * #### Notes
220 * A message hook is invoked before a message is delivered to the
221 * handler. If the hook returns `false`, no other hooks will be
222 * invoked and the message will not be delivered to the handler.
223 *
224 * The most recently installed message hook is executed first.
225 *
226 * If the hook is already installed, this is a no-op.
227 */
228 function installMessageHook(handler, hook) {
229 // Look up the hooks for the handler.
230 let hooks = messageHooks.get(handler);
231 // Bail early if the hook is already installed.
232 if (hooks && hooks.indexOf(hook) !== -1) {
233 return;
234 }
235 // Add the hook to the end, so it will be the first to execute.
236 if (!hooks) {
237 messageHooks.set(handler, [hook]);
238 }
239 else {
240 hooks.push(hook);
241 }
242 }
243 MessageLoop.installMessageHook = installMessageHook;
244 /**
245 * Remove an installed message hook for a message handler.
246 *
247 * @param handler - The message handler of interest.
248 *
249 * @param hook - The message hook to remove.
250 *
251 * #### Notes
252 * It is safe to call this function while the hook is executing.
253 *
254 * If the hook is not installed, this is a no-op.
255 */
256 function removeMessageHook(handler, hook) {
257 // Lookup the hooks for the handler.
258 let hooks = messageHooks.get(handler);
259 // Bail early if the hooks do not exist.
260 if (!hooks) {
261 return;
262 }
263 // Lookup the index of the hook and bail if not found.
264 let i = hooks.indexOf(hook);
265 if (i === -1) {
266 return;
267 }
268 // Clear the hook and schedule a cleanup of the array.
269 hooks[i] = null;
270 scheduleCleanup(hooks);
271 }
272 MessageLoop.removeMessageHook = removeMessageHook;
273 /**
274 * Clear all message data associated with a message handler.
275 *
276 * @param handler - The message handler of interest.
277 *
278 * #### Notes
279 * This will clear all posted messages and hooks for the handler.
280 */
281 function clearData(handler) {
282 // Lookup the hooks for the handler.
283 let hooks = messageHooks.get(handler);
284 // Clear all messsage hooks for the handler.
285 if (hooks && hooks.length > 0) {
286 algorithm.ArrayExt.fill(hooks, null);
287 scheduleCleanup(hooks);
288 }
289 // Clear all posted messages for the handler.
290 for (const posted of messageQueue) {
291 if (posted.handler === handler) {
292 posted.handler = null;
293 posted.msg = null;
294 }
295 }
296 }
297 MessageLoop.clearData = clearData;
298 /**
299 * Process the pending posted messages in the queue immediately.
300 *
301 * #### Notes
302 * This function is useful when posted messages must be processed immediately.
303 *
304 * This function should normally not be needed, but it may be
305 * required to work around certain browser idiosyncrasies.
306 *
307 * Recursing into this function is a no-op.
308 */
309 function flush() {
310 // Bail if recursion is detected or if there is no pending task.
311 if (flushGuard || pending === null) {
312 return;
313 }
314 // Unschedule the pending loop task.
315 pending();
316 pending = null;
317 // Run the message loop within the recursion guard.
318 flushGuard = true;
319 runMessageLoop();
320 flushGuard = false;
321 }
322 MessageLoop.flush = flush;
323 /**
324 * Get the message loop exception handler.
325 *
326 * @returns The current exception handler.
327 *
328 * #### Notes
329 * The default exception handler is `console.error`.
330 */
331 function getExceptionHandler() {
332 return exceptionHandler;
333 }
334 MessageLoop.getExceptionHandler = getExceptionHandler;
335 /**
336 * Set the message loop exception handler.
337 *
338 * @param handler - The function to use as the exception handler.
339 *
340 * @returns The old exception handler.
341 *
342 * #### Notes
343 * The exception handler is invoked when a message handler or a
344 * message hook throws an exception.
345 */
346 function setExceptionHandler(handler) {
347 let old = exceptionHandler;
348 exceptionHandler = handler;
349 return old;
350 }
351 MessageLoop.setExceptionHandler = setExceptionHandler;
352 /**
353 * The queue of posted message pairs.
354 */
355 const messageQueue = new collections.LinkedList();
356 /**
357 * A mapping of handler to array of installed message hooks.
358 */
359 const messageHooks = new WeakMap();
360 /**
361 * A set of message hook arrays which are pending cleanup.
362 */
363 const dirtySet = new Set();
364 /**
365 * The message loop exception handler.
366 */
367 let exceptionHandler = (err) => {
368 console.error(err);
369 };
370 /**
371 * A guard flag to prevent flush recursion.
372 */
373 let flushGuard = false;
374 /**
375 * Invoke a message hook with the specified handler and message.
376 *
377 * Returns the result of the hook, or `true` if the hook throws.
378 *
379 * Exceptions in the hook will be caught and logged.
380 */
381 function invokeHook(hook, handler, msg) {
382 let result = true;
383 try {
384 if (typeof hook === 'function') {
385 result = hook(handler, msg);
386 }
387 else {
388 result = hook.messageHook(handler, msg);
389 }
390 }
391 catch (err) {
392 exceptionHandler(err);
393 }
394 return result;
395 }
396 /**
397 * Invoke a message handler with the specified message.
398 *
399 * Exceptions in the handler will be caught and logged.
400 */
401 function invokeHandler(handler, msg) {
402 try {
403 handler.processMessage(msg);
404 }
405 catch (err) {
406 exceptionHandler(err);
407 }
408 }
409 /**
410 * Add a message to the end of the message queue.
411 *
412 * This will automatically schedule a run of the message loop.
413 */
414 function enqueueMessage(handler, msg) {
415 // Add the posted message to the queue.
416 messageQueue.addLast({ handler, msg });
417 // Bail if a loop task is already pending.
418 if (pending !== null) {
419 return;
420 }
421 // Schedule a run of the message loop.
422 pending = schedule(runMessageLoop);
423 }
424 /**
425 * Run an iteration of the message loop.
426 *
427 * This will process all pending messages in the queue. If a message
428 * is added to the queue while the message loop is running, it will
429 * be processed on the next cycle of the loop.
430 */
431 function runMessageLoop() {
432 // Clear the task so the next loop can be scheduled.
433 pending = null;
434 // If the message queue is empty, there is nothing else to do.
435 if (messageQueue.isEmpty) {
436 return;
437 }
438 // Add a sentinel value to the end of the queue. The queue will
439 // only be processed up to the sentinel. Messages posted during
440 // this cycle will execute on the next cycle.
441 let sentinel = { handler: null, msg: null };
442 messageQueue.addLast(sentinel);
443 // Enter the message loop.
444 // eslint-disable-next-line no-constant-condition
445 while (true) {
446 // Remove the first posted message in the queue.
447 let posted = messageQueue.removeFirst();
448 // If the value is the sentinel, exit the loop.
449 if (posted === sentinel) {
450 return;
451 }
452 // Dispatch the message if it has not been cleared.
453 if (posted.handler && posted.msg) {
454 sendMessage(posted.handler, posted.msg);
455 }
456 }
457 }
458 /**
459 * Schedule a cleanup of a message hooks array.
460 *
461 * This will add the array to the dirty set and schedule a deferred
462 * cleanup of the array contents. On cleanup, any `null` hook will
463 * be removed from the array.
464 */
465 function scheduleCleanup(hooks) {
466 if (dirtySet.size === 0) {
467 schedule(cleanupDirtySet);
468 }
469 dirtySet.add(hooks);
470 }
471 /**
472 * Cleanup the message hook arrays in the dirty set.
473 *
474 * This function should only be invoked asynchronously, when the
475 * stack frame is guaranteed to not be on the path of user code.
476 */
477 function cleanupDirtySet() {
478 dirtySet.forEach(cleanupHooks);
479 dirtySet.clear();
480 }
481 /**
482 * Cleanup the dirty hooks in a message hooks array.
483 *
484 * This will remove any `null` hook from the array.
485 *
486 * This function should only be invoked asynchronously, when the
487 * stack frame is guaranteed to not be on the path of user code.
488 */
489 function cleanupHooks(hooks) {
490 algorithm.ArrayExt.removeAllWhere(hooks, isNull);
491 }
492 /**
493 * Test whether a value is `null`.
494 */
495 function isNull(value) {
496 return value === null;
497 }
498 })(exports.MessageLoop || (exports.MessageLoop = {}));
499
500 exports.ConflatableMessage = ConflatableMessage;
501 exports.Message = Message;
502
503}));
504//# sourceMappingURL=index.js.map