forked from jupyterlab/lumino
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
790 lines (714 loc) · 21.1 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
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
// 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.
|----------------------------------------------------------------------------*/
import { ArrayExt, find } from '@lumino/algorithm';
import { PromiseDelegate } from '@lumino/coreutils';
import { AttachedProperty } from '@lumino/properties';
/**
* A type alias for a slot function.
*
* @param sender - The object emitting the signal.
*
* @param args - The args object emitted with the signal.
*
* #### Notes
* A slot is invoked when a signal to which it is connected is emitted.
*/
export type Slot<T, U> = (sender: T, args: U) => void;
/**
* An object used for type-safe inter-object communication.
*
* #### Notes
* Signals provide a type-safe implementation of the publish-subscribe
* pattern. An object (publisher) declares which signals it will emit,
* and consumers connect callbacks (subscribers) to those signals. The
* subscribers are invoked whenever the publisher emits the signal.
*/
export interface ISignal<T, U> {
/**
* Block the signal during the execution of a callback.
*
* ### Notes
* The callback function must be synchronous.
*
* @param fn The callback during which the signal is blocked
*/
block(fn: () => void): void;
/**
* Connect a slot to the signal.
*
* @param slot - The slot to invoke when the signal is emitted.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection succeeds, `false` otherwise.
*
* #### Notes
* Slots are invoked in the order in which they are connected.
*
* Signal connections are unique. If a connection already exists for
* the given `slot` and `thisArg`, this method returns `false`.
*
* A newly connected slot will not be invoked until the next time the
* signal is emitted, even if the slot is connected while the signal
* is dispatching.
*/
connect(slot: Slot<T, U>, thisArg?: any): boolean;
/**
* Disconnect a slot from the signal.
*
* @param slot - The slot to disconnect from the signal.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection is removed, `false` otherwise.
*
* #### Notes
* If no connection exists for the given `slot` and `thisArg`, this
* method returns `false`.
*
* A disconnected slot will no longer be invoked, even if the slot
* is disconnected while the signal is dispatching.
*/
disconnect(slot: Slot<T, U>, thisArg?: any): boolean;
}
/**
* An object that is both a signal and an async iterable.
*/
export interface IStream<T, U> extends ISignal<T, U>, AsyncIterable<U> {}
/**
* A concrete implementation of `ISignal`.
*
* #### Example
* ```typescript
* import { ISignal, Signal } from '@lumino/signaling';
*
* class SomeClass {
*
* constructor(name: string) {
* this.name = name;
* }
*
* readonly name: string;
*
* get valueChanged: ISignal<this, number> {
* return this._valueChanged;
* }
*
* get value(): number {
* return this._value;
* }
*
* set value(value: number) {
* if (value === this._value) {
* return;
* }
* this._value = value;
* this._valueChanged.emit(value);
* }
*
* private _value = 0;
* private _valueChanged = new Signal<this, number>(this);
* }
*
* function logger(sender: SomeClass, value: number): void {
* console.log(sender.name, value);
* }
*
* let m1 = new SomeClass('foo');
* let m2 = new SomeClass('bar');
*
* m1.valueChanged.connect(logger);
* m2.valueChanged.connect(logger);
*
* m1.value = 42; // logs: foo 42
* m2.value = 17; // logs: bar 17
* ```
*/
export class Signal<T, U> implements ISignal<T, U> {
/**
* Construct a new signal.
*
* @param sender - The sender which owns the signal.
*/
constructor(sender: T) {
this.sender = sender;
}
/**
* The sender which owns the signal.
*/
readonly sender: T;
/**
* Block the signal during the execution of a callback.
*
* ### Notes
* The callback function must be synchronous.
*
* @param fn The callback during which the signal is blocked
*/
block(fn: () => void): void {
this.blocked++;
try {
fn();
} finally {
this.blocked--;
}
}
/**
* Connect a slot to the signal.
*
* @param slot - The slot to invoke when the signal is emitted.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection succeeds, `false` otherwise.
*/
connect(slot: Slot<T, U>, thisArg?: unknown): boolean {
return Private.connect(this, slot, thisArg);
}
/**
* Disconnect a slot from the signal.
*
* @param slot - The slot to disconnect from the signal.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection is removed, `false` otherwise.
*/
disconnect(slot: Slot<T, U>, thisArg?: unknown): boolean {
return Private.disconnect(this, slot, thisArg);
}
/**
* Emit the signal and invoke the connected slots.
*
* @param args - The args to pass to the connected slots.
*
* #### Notes
* Slots are invoked synchronously in connection order.
*
* Exceptions thrown by connected slots will be caught and logged.
*/
emit(args: U): void {
if (!this.blocked) {
Private.emit(this, args);
}
}
/**
* If `blocked` is not `0`, the signal will not emit.
*/
protected blocked = 0;
}
/**
* The namespace for the `Signal` class statics.
*/
export namespace Signal {
/**
* Block all signals emitted by an object during
* the execution of a callback.
*
* ### Notes
* The callback function must be synchronous.
*
* @param sender The signals sender
* @param fn The callback during which all signals are blocked
*/
export function blockAll(sender: unknown, fn: () => void): void {
const { blockedProperty } = Private;
blockedProperty.set(sender, blockedProperty.get(sender) + 1);
try {
fn();
} finally {
blockedProperty.set(sender, blockedProperty.get(sender) - 1);
}
}
/**
* Remove all connections between a sender and receiver.
*
* @param sender - The sender object of interest.
*
* @param receiver - The receiver object of interest.
*
* #### Notes
* If a `thisArg` is provided when connecting a signal, that object
* is considered the receiver. Otherwise, the `slot` is considered
* the receiver.
*/
export function disconnectBetween(sender: unknown, receiver: unknown): void {
Private.disconnectBetween(sender, receiver);
}
/**
* Remove all connections where the given object is the sender.
*
* @param sender - The sender object of interest.
*/
export function disconnectSender(sender: unknown): void {
Private.disconnectSender(sender);
}
/**
* Remove all connections where the given object is the receiver.
*
* @param receiver - The receiver object of interest.
*
* #### Notes
* If a `thisArg` is provided when connecting a signal, that object
* is considered the receiver. Otherwise, the `slot` is considered
* the receiver.
*/
export function disconnectReceiver(receiver: unknown): void {
Private.disconnectReceiver(receiver);
}
/**
* Remove all connections where an object is the sender or receiver.
*
* @param object - The object of interest.
*
* #### Notes
* If a `thisArg` is provided when connecting a signal, that object
* is considered the receiver. Otherwise, the `slot` is considered
* the receiver.
*/
export function disconnectAll(object: unknown): void {
Private.disconnectAll(object);
}
/**
* Clear all signal data associated with the given object.
*
* @param object - The object for which the data should be cleared.
*
* #### Notes
* This removes all signal connections and any other signal data
* associated with the object.
*/
export function clearData(object: unknown): void {
Private.disconnectAll(object);
}
/**
* A type alias for the exception handler function.
*/
export type ExceptionHandler = (err: Error) => void;
/**
* Get the signal exception handler.
*
* @returns The current exception handler.
*
* #### Notes
* The default exception handler is `console.error`.
*/
export function getExceptionHandler(): ExceptionHandler {
return Private.exceptionHandler;
}
/**
* Set the signal 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 slot throws an exception.
*/
export function setExceptionHandler(
handler: ExceptionHandler
): ExceptionHandler {
let old = Private.exceptionHandler;
Private.exceptionHandler = handler;
return old;
}
}
/**
* A stream with the characteristics of a signal and an async iterable.
*/
export class Stream<T, U> extends Signal<T, U> implements IStream<T, U> {
/**
* Return an async iterator that yields every emission.
*/
async *[Symbol.asyncIterator](): AsyncIterableIterator<U> {
let pending = this._pending;
while (true) {
try {
const { args, next } = await pending.promise;
pending = next;
yield args;
} catch (_) {
return; // Any promise rejection stops the iterator.
}
}
}
/**
* Emit the signal, invoke the connected slots, and yield the emission.
*
* @param args - The args to pass to the connected slots.
*/
emit(args: U): void {
if (!this.blocked) {
const pending = this._pending;
this._pending = new PromiseDelegate();
pending.resolve({ args, next: this._pending });
super.emit(args);
}
}
/**
* Stop the stream's async iteration.
*/
stop(): void {
this._pending.promise.catch(() => undefined);
this._pending.reject('stop');
}
private _pending: Private.Pending<U> = new PromiseDelegate();
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* A pending promise in a promise chain underlying a stream.
*/
export type Pending<U> = PromiseDelegate<{ args: U; next: Pending<U> }>;
/**
* The signal exception handler function.
*/
export let exceptionHandler: Signal.ExceptionHandler = (err: Error) => {
console.error(err);
};
/**
* Connect a slot to a signal.
*
* @param signal - The signal of interest.
*
* @param slot - The slot to invoke when the signal is emitted.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection succeeds, `false` otherwise.
*/
export function connect<T, U>(
signal: Signal<T, U>,
slot: Slot<T, U>,
thisArg?: unknown
): boolean {
// Coerce a `null` `thisArg` to `undefined`.
thisArg = thisArg || undefined;
// Ensure the sender's array of receivers is created.
let receivers = receiversForSender.get(signal.sender);
if (!receivers) {
receivers = [];
receiversForSender.set(signal.sender, receivers);
}
// Bail if a matching connection already exists.
if (findConnection(receivers, signal, slot, thisArg)) {
return false;
}
// Choose the best object for the receiver.
let receiver = thisArg || slot;
// Ensure the receiver's array of senders is created.
let senders = sendersForReceiver.get(receiver);
if (!senders) {
senders = [];
sendersForReceiver.set(receiver, senders);
}
// Create a new connection and add it to the end of each array.
let connection = { signal, slot, thisArg };
receivers.push(connection);
senders.push(connection);
// Indicate a successful connection.
return true;
}
/**
* Disconnect a slot from a signal.
*
* @param signal - The signal of interest.
*
* @param slot - The slot to disconnect from the signal.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection is removed, `false` otherwise.
*/
export function disconnect<T, U>(
signal: Signal<T, U>,
slot: Slot<T, U>,
thisArg?: unknown
): boolean {
// Coerce a `null` `thisArg` to `undefined`.
thisArg = thisArg || undefined;
// Lookup the list of receivers, and bail if none exist.
let receivers = receiversForSender.get(signal.sender);
if (!receivers || receivers.length === 0) {
return false;
}
// Bail if no matching connection exits.
let connection = findConnection(receivers, signal, slot, thisArg);
if (!connection) {
return false;
}
// Choose the best object for the receiver.
let receiver = thisArg || slot;
// Lookup the array of senders, which is now known to exist.
let senders = sendersForReceiver.get(receiver)!;
// Clear the connection and schedule cleanup of the arrays.
connection.signal = null;
scheduleCleanup(receivers);
scheduleCleanup(senders);
// Indicate a successful disconnection.
return true;
}
/**
* Remove all connections between a sender and receiver.
*
* @param sender - The sender object of interest.
*
* @param receiver - The receiver object of interest.
*/
export function disconnectBetween(sender: unknown, receiver: unknown): void {
// If there are no receivers, there is nothing to do.
let receivers = receiversForSender.get(sender);
if (!receivers || receivers.length === 0) {
return;
}
// If there are no senders, there is nothing to do.
let senders = sendersForReceiver.get(receiver);
if (!senders || senders.length === 0) {
return;
}
// Clear each connection between the sender and receiver.
for (const connection of senders) {
// Skip connections which have already been cleared.
if (!connection.signal) {
continue;
}
// Clear the connection if it matches the sender.
if (connection.signal.sender === sender) {
connection.signal = null;
}
}
// Schedule a cleanup of the senders and receivers.
scheduleCleanup(receivers);
scheduleCleanup(senders);
}
/**
* Remove all connections where the given object is the sender.
*
* @param sender - The sender object of interest.
*/
export function disconnectSender(sender: unknown): void {
// If there are no receivers, there is nothing to do.
let receivers = receiversForSender.get(sender);
if (!receivers || receivers.length === 0) {
return;
}
// Clear each receiver connection.
for (const connection of receivers) {
// Skip connections which have already been cleared.
if (!connection.signal) {
continue;
}
// Choose the best object for the receiver.
let receiver = connection.thisArg || connection.slot;
// Clear the connection.
connection.signal = null;
// Cleanup the array of senders, which is now known to exist.
scheduleCleanup(sendersForReceiver.get(receiver)!);
}
// Schedule a cleanup of the receivers.
scheduleCleanup(receivers);
}
/**
* Remove all connections where the given object is the receiver.
*
* @param receiver - The receiver object of interest.
*/
export function disconnectReceiver(receiver: unknown): void {
// If there are no senders, there is nothing to do.
let senders = sendersForReceiver.get(receiver);
if (!senders || senders.length === 0) {
return;
}
// Clear each sender connection.
for (const connection of senders) {
// Skip connections which have already been cleared.
if (!connection.signal) {
continue;
}
// Lookup the sender for the connection.
let sender = connection.signal.sender;
// Clear the connection.
connection.signal = null;
// Cleanup the array of receivers, which is now known to exist.
scheduleCleanup(receiversForSender.get(sender)!);
}
// Schedule a cleanup of the list of senders.
scheduleCleanup(senders);
}
/**
* Remove all connections where an object is the sender or receiver.
*
* @param object - The object of interest.
*/
export function disconnectAll(object: unknown): void {
// Remove all connections where the given object is the sender.
disconnectSender(object);
// Remove all connections where the given object is the receiver.
disconnectReceiver(object);
}
/**
* Emit a signal and invoke its connected slots.
*
* @param signal - The signal of interest.
*
* @param args - The args to pass to the connected slots.
*
* #### Notes
* Slots are invoked synchronously in connection order.
*
* Exceptions thrown by connected slots will be caught and logged.
*/
export function emit<T, U>(signal: Signal<T, U>, args: U): void {
if (Private.blockedProperty.get(signal.sender) > 0) {
return;
}
// If there are no receivers, there is nothing to do.
let receivers = receiversForSender.get(signal.sender);
if (!receivers || receivers.length === 0) {
return;
}
// Invoke the slots for connections with a matching signal.
// Any connections added during emission are not invoked.
for (let i = 0, n = receivers.length; i < n; ++i) {
let connection = receivers[i];
if (connection.signal === signal) {
invokeSlot(connection, args);
}
}
}
/**
* An object which holds connection data.
*/
interface IConnection {
/**
* The signal for the connection.
*
* A `null` signal indicates a cleared connection.
*/
signal: Signal<any, any> | null;
/**
* The slot connected to the signal.
*/
readonly slot: Slot<any, any>;
/**
* The `this` context for the slot.
*/
readonly thisArg: any;
}
/**
* A weak mapping of sender to array of receiver connections.
*/
const receiversForSender = new WeakMap<any, IConnection[]>();
/**
* A weak mapping of receiver to array of sender connections.
*/
const sendersForReceiver = new WeakMap<any, IConnection[]>();
/**
* A set of connection arrays which are pending cleanup.
*/
const dirtySet = new Set<IConnection[]>();
/**
* A function to schedule an event loop callback.
*/
const schedule = (() => {
let ok = typeof requestAnimationFrame === 'function';
return ok ? requestAnimationFrame : setImmediate;
})();
/**
* Find a connection which matches the given parameters.
*/
function findConnection(
connections: IConnection[],
signal: Signal<any, any>,
slot: Slot<any, any>,
thisArg: any
): IConnection | undefined {
return find(
connections,
connection =>
connection.signal === signal &&
connection.slot === slot &&
connection.thisArg === thisArg
);
}
/**
* Invoke a slot with the given parameters.
*
* The connection is assumed to be valid.
*
* Exceptions in the slot will be caught and logged.
*/
function invokeSlot(connection: IConnection, args: any): void {
let { signal, slot, thisArg } = connection;
try {
slot.call(thisArg, signal!.sender, args);
} catch (err) {
exceptionHandler(err);
}
}
/**
* Schedule a cleanup of a connection array.
*
* This will add the array to the dirty set and schedule a deferred
* cleanup of the array contents. On cleanup, any connection with a
* `null` signal will be removed from the array.
*/
function scheduleCleanup(array: IConnection[]): void {
if (dirtySet.size === 0) {
schedule(cleanupDirtySet);
}
dirtySet.add(array);
}
/**
* Cleanup the connection lists 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(cleanupConnections);
dirtySet.clear();
}
/**
* Cleanup the dirty connections in a connections array.
*
* This will remove any connection with a `null` signal.
*
* This function should only be invoked asynchronously, when the
* stack frame is guaranteed to not be on the path of user code.
*/
function cleanupConnections(connections: IConnection[]): void {
ArrayExt.removeAllWhere(connections, isDeadConnection);
}
/**
* Test whether a connection is dead.
*
* A dead connection has a `null` signal.
*/
function isDeadConnection(connection: IConnection): boolean {
return connection.signal === null;
}
/**
* A property indicating a sender has been blocked if its value is not 0.
*/
export const blockedProperty = new AttachedProperty<unknown, number>({
name: 'blocked',
create: () => 0
});
}