/
index.ts
641 lines (585 loc) · 17.8 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module messaging
*/
import { ArrayExt, every, retro, some } from '@lumino/algorithm';
import { LinkedList } from '@lumino/collections';
/**
* A message which can be delivered to a message handler.
*
* #### Notes
* This class may be subclassed to create complex message types.
*/
export class Message {
/**
* Construct a new message.
*
* @param type - The type of the message.
*/
constructor(type: string) {
this.type = type;
}
/**
* The type of the message.
*
* #### Notes
* The `type` of a message should be related directly to its actual
* runtime type. This means that `type` can and will be used to cast
* the message to the relevant derived `Message` subtype.
*/
readonly type: string;
/**
* Test whether the message is conflatable.
*
* #### Notes
* Message conflation is an advanced topic. Most message types will
* not make use of this feature.
*
* If a conflatable message is posted to a handler while another
* conflatable message of the same `type` has already been posted
* to the handler, the `conflate()` method of the existing message
* will be invoked. If that method returns `true`, the new message
* will not be enqueued. This allows messages to be compressed, so
* that only a single instance of the message type is processed per
* cycle, no matter how many times messages of that type are posted.
*
* Custom message types may reimplement this property.
*
* The default implementation is always `false`.
*/
get isConflatable(): boolean {
return false;
}
/**
* Conflate this message with another message of the same `type`.
*
* @param other - A conflatable message of the same `type`.
*
* @returns `true` if the message was successfully conflated, or
* `false` otherwise.
*
* #### Notes
* Message conflation is an advanced topic. Most message types will
* not make use of this feature.
*
* This method is called automatically by the message loop when the
* given message is posted to the handler paired with this message.
* This message will already be enqueued and conflatable, and the
* given message will have the same `type` and also be conflatable.
*
* This method should merge the state of the other message into this
* message as needed so that when this message is finally delivered
* to the handler, it receives the most up-to-date information.
*
* If this method returns `true`, it signals that the other message
* was successfully conflated and that message will not be enqueued.
*
* If this method returns `false`, the other message will be enqueued
* for normal delivery.
*
* Custom message types may reimplement this method.
*
* The default implementation always returns `false`.
*/
conflate(other: Message): boolean {
return false;
}
}
/**
* A convenience message class which conflates automatically.
*
* #### Notes
* Message conflation is an advanced topic. Most user code will not
* make use of this class.
*
* This message class is useful for creating message instances which
* should be conflated, but which have no state other than `type`.
*
* If conflation of stateful messages is required, a custom `Message`
* subclass should be created.
*/
export class ConflatableMessage extends Message {
/**
* Test whether the message is conflatable.
*
* #### Notes
* This property is always `true`.
*/
get isConflatable(): boolean {
return true;
}
/**
* Conflate this message with another message of the same `type`.
*
* #### Notes
* This method always returns `true`.
*/
conflate(other: ConflatableMessage): boolean {
return true;
}
}
/**
* An object which handles messages.
*
* #### Notes
* A message handler is a simple way of defining a type which can act
* upon on a large variety of external input without requiring a large
* abstract API surface. This is particularly useful in the context of
* widget frameworks where the number of distinct message types can be
* unbounded.
*/
export interface IMessageHandler {
/**
* Process a message sent to the handler.
*
* @param msg - The message to be processed.
*/
processMessage(msg: Message): void;
}
/**
* An object which intercepts messages sent to a message handler.
*
* #### Notes
* A message hook is useful for intercepting or spying on messages
* sent to message handlers which were either not created by the
* consumer, or when subclassing the handler is not feasible.
*
* If `messageHook` returns `false`, no other message hooks will be
* invoked and the message will not be delivered to the handler.
*
* If all installed message hooks return `true`, the message will
* be delivered to the handler for processing.
*
* **See also:** {@link MessageLoop.installMessageHook} and {@link MessageLoop.removeMessageHook}
*/
export interface IMessageHook {
/**
* Intercept a message sent to a message handler.
*
* @param handler - The target handler of the message.
*
* @param msg - The message to be sent to the handler.
*
* @returns `true` if the message should continue to be processed
* as normal, or `false` if processing should cease immediately.
*/
messageHook(handler: IMessageHandler, msg: Message): boolean;
}
/**
* A type alias for message hook object or function.
*
* #### Notes
* The signature and semantics of a message hook function are the same
* as the `messageHook` method of {@link IMessageHook}.
*/
export type MessageHook =
| IMessageHook
| ((handler: IMessageHandler, msg: Message) => boolean);
/**
* The namespace for the global singleton message loop.
*/
export namespace MessageLoop {
/**
* A function that cancels the pending loop task; `null` if unavailable.
*/
let pending: (() => void) | null = null;
/**
* Schedules a function for invocation as soon as possible asynchronously.
*
* @param fn The function to invoke when called back.
*
* @returns An anonymous function that will unschedule invocation if possible.
*/
const schedule = (
resolved =>
(fn: () => unknown): (() => void) => {
let rejected = false;
resolved.then(() => !rejected && fn());
return () => {
rejected = true;
};
}
)(Promise.resolve());
/**
* Send a message to a message handler to process immediately.
*
* @param handler - The handler which should process the message.
*
* @param msg - The message to deliver to the handler.
*
* #### Notes
* The message will first be sent through any installed message hooks
* for the handler. If the message passes all hooks, it will then be
* delivered to the `processMessage` method of the handler.
*
* The message will not be conflated with pending posted messages.
*
* Exceptions in hooks and handlers will be caught and logged.
*/
export function sendMessage(handler: IMessageHandler, msg: Message): void {
// Lookup the message hooks for the handler.
let hooks = messageHooks.get(handler);
// Handle the common case of no installed hooks.
if (!hooks || hooks.length === 0) {
invokeHandler(handler, msg);
return;
}
// Invoke the message hooks starting with the newest first.
let passed = every(retro(hooks), hook => {
return hook ? invokeHook(hook, handler, msg) : true;
});
// Invoke the handler if the message passes all hooks.
if (passed) {
invokeHandler(handler, msg);
}
}
/**
* Post a message to a message handler to process in the future.
*
* @param handler - The handler which should process the message.
*
* @param msg - The message to post to the handler.
*
* #### Notes
* The message will be conflated with the pending posted messages for
* the handler, if possible. If the message is not conflated, it will
* be queued for normal delivery on the next cycle of the event loop.
*
* Exceptions in hooks and handlers will be caught and logged.
*/
export function postMessage(handler: IMessageHandler, msg: Message): void {
// Handle the common case of a non-conflatable message.
if (!msg.isConflatable) {
enqueueMessage(handler, msg);
return;
}
// Conflate the message with an existing message if possible.
let conflated = some(messageQueue, posted => {
if (posted.handler !== handler) {
return false;
}
if (!posted.msg) {
return false;
}
if (posted.msg.type !== msg.type) {
return false;
}
if (!posted.msg.isConflatable) {
return false;
}
return posted.msg.conflate(msg);
});
// Enqueue the message if it was not conflated.
if (!conflated) {
enqueueMessage(handler, msg);
}
}
/**
* Install a message hook for a message handler.
*
* @param handler - The message handler of interest.
*
* @param hook - The message hook to install.
*
* #### Notes
* A message hook is invoked before a message is delivered to the
* handler. If the hook returns `false`, no other hooks will be
* invoked and the message will not be delivered to the handler.
*
* The most recently installed message hook is executed first.
*
* If the hook is already installed, this is a no-op.
*/
export function installMessageHook(
handler: IMessageHandler,
hook: MessageHook
): void {
// Look up the hooks for the handler.
let hooks = messageHooks.get(handler);
// Bail early if the hook is already installed.
if (hooks && hooks.indexOf(hook) !== -1) {
return;
}
// Add the hook to the end, so it will be the first to execute.
if (!hooks) {
messageHooks.set(handler, [hook]);
} else {
hooks.push(hook);
}
}
/**
* Remove an installed message hook for a message handler.
*
* @param handler - The message handler of interest.
*
* @param hook - The message hook to remove.
*
* #### Notes
* It is safe to call this function while the hook is executing.
*
* If the hook is not installed, this is a no-op.
*/
export function removeMessageHook(
handler: IMessageHandler,
hook: MessageHook
): void {
// Lookup the hooks for the handler.
let hooks = messageHooks.get(handler);
// Bail early if the hooks do not exist.
if (!hooks) {
return;
}
// Lookup the index of the hook and bail if not found.
let i = hooks.indexOf(hook);
if (i === -1) {
return;
}
// Clear the hook and schedule a cleanup of the array.
hooks[i] = null;
scheduleCleanup(hooks);
}
/**
* Clear all message data associated with a message handler.
*
* @param handler - The message handler of interest.
*
* #### Notes
* This will clear all posted messages and hooks for the handler.
*/
export function clearData(handler: IMessageHandler): void {
// Lookup the hooks for the handler.
let hooks = messageHooks.get(handler);
// Clear all messsage hooks for the handler.
if (hooks && hooks.length > 0) {
ArrayExt.fill(hooks, null);
scheduleCleanup(hooks);
}
// Clear all posted messages for the handler.
for (const posted of messageQueue) {
if (posted.handler === handler) {
posted.handler = null;
posted.msg = null;
}
}
}
/**
* Process the pending posted messages in the queue immediately.
*
* #### Notes
* This function is useful when posted messages must be processed immediately.
*
* This function should normally not be needed, but it may be
* required to work around certain browser idiosyncrasies.
*
* Recursing into this function is a no-op.
*/
export function flush(): void {
// Bail if recursion is detected or if there is no pending task.
if (flushGuard || pending === null) {
return;
}
// Unschedule the pending loop task.
pending();
pending = null;
// Run the message loop within the recursion guard.
flushGuard = true;
runMessageLoop();
flushGuard = false;
}
/**
* A type alias for the exception handler function.
*/
export type ExceptionHandler = (err: Error) => void;
/**
* Get the message loop exception handler.
*
* @returns The current exception handler.
*
* #### Notes
* The default exception handler is `console.error`.
*/
export function getExceptionHandler(): ExceptionHandler {
return exceptionHandler;
}
/**
* Set the message loop exception handler.
*
* @param handler - The function to use as the exception handler.
*
* @returns The old exception handler.
*
* #### Notes
* The exception handler is invoked when a message handler or a
* message hook throws an exception.
*/
export function setExceptionHandler(
handler: ExceptionHandler
): ExceptionHandler {
let old = exceptionHandler;
exceptionHandler = handler;
return old;
}
/**
* A type alias for a posted message pair.
*/
type PostedMessage = { handler: IMessageHandler | null; msg: Message | null };
/**
* The queue of posted message pairs.
*/
const messageQueue = new LinkedList<PostedMessage>();
/**
* A mapping of handler to array of installed message hooks.
*/
const messageHooks = new WeakMap<
IMessageHandler,
Array<MessageHook | null>
>();
/**
* A set of message hook arrays which are pending cleanup.
*/
const dirtySet = new Set<Array<MessageHook | null>>();
/**
* The message loop exception handler.
*/
let exceptionHandler: ExceptionHandler = (err: Error) => {
console.error(err);
};
/**
* A guard flag to prevent flush recursion.
*/
let flushGuard = false;
/**
* Invoke a message hook with the specified handler and message.
*
* Returns the result of the hook, or `true` if the hook throws.
*
* Exceptions in the hook will be caught and logged.
*/
function invokeHook(
hook: MessageHook,
handler: IMessageHandler,
msg: Message
): boolean {
let result = true;
try {
if (typeof hook === 'function') {
result = hook(handler, msg);
} else {
result = hook.messageHook(handler, msg);
}
} catch (err) {
exceptionHandler(err);
}
return result;
}
/**
* Invoke a message handler with the specified message.
*
* Exceptions in the handler will be caught and logged.
*/
function invokeHandler(handler: IMessageHandler, msg: Message): void {
try {
handler.processMessage(msg);
} catch (err) {
exceptionHandler(err);
}
}
/**
* Add a message to the end of the message queue.
*
* This will automatically schedule a run of the message loop.
*/
function enqueueMessage(handler: IMessageHandler, msg: Message): void {
// Add the posted message to the queue.
messageQueue.addLast({ handler, msg });
// Bail if a loop task is already pending.
if (pending !== null) {
return;
}
// Schedule a run of the message loop.
pending = schedule(runMessageLoop);
}
/**
* Run an iteration of the message loop.
*
* This will process all pending messages in the queue. If a message
* is added to the queue while the message loop is running, it will
* be processed on the next cycle of the loop.
*/
function runMessageLoop(): void {
// Clear the task so the next loop can be scheduled.
pending = null;
// If the message queue is empty, there is nothing else to do.
if (messageQueue.isEmpty) {
return;
}
// Add a sentinel value to the end of the queue. The queue will
// only be processed up to the sentinel. Messages posted during
// this cycle will execute on the next cycle.
let sentinel: PostedMessage = { handler: null, msg: null };
messageQueue.addLast(sentinel);
// Enter the message loop.
// eslint-disable-next-line no-constant-condition
while (true) {
// Remove the first posted message in the queue.
let posted = messageQueue.removeFirst()!;
// If the value is the sentinel, exit the loop.
if (posted === sentinel) {
return;
}
// Dispatch the message if it has not been cleared.
if (posted.handler && posted.msg) {
sendMessage(posted.handler, posted.msg);
}
}
}
/**
* Schedule a cleanup of a message hooks array.
*
* This will add the array to the dirty set and schedule a deferred
* cleanup of the array contents. On cleanup, any `null` hook will
* be removed from the array.
*/
function scheduleCleanup(hooks: Array<MessageHook | null>): void {
if (dirtySet.size === 0) {
schedule(cleanupDirtySet);
}
dirtySet.add(hooks);
}
/**
* Cleanup the message hook arrays in the dirty set.
*
* This function should only be invoked asynchronously, when the
* stack frame is guaranteed to not be on the path of user code.
*/
function cleanupDirtySet(): void {
dirtySet.forEach(cleanupHooks);
dirtySet.clear();
}
/**
* Cleanup the dirty hooks in a message hooks array.
*
* This will remove any `null` hook from the array.
*
* This function should only be invoked asynchronously, when the
* stack frame is guaranteed to not be on the path of user code.
*/
function cleanupHooks(hooks: Array<MessageHook | null>): void {
ArrayExt.removeAllWhere(hooks, isNull);
}
/**
* Test whether a value is `null`.
*/
function isNull<T>(value: T | null): boolean {
return value === null;
}
}