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
|