1 // Written in the D programming language.
2 
3 /**
4  * Signals and Slots are an implementation of the Observer Pattern.
5  * Essentially, when a Signal is emitted, a list of connected Observers
6  * (called slots) are called.
7  *
8  * There have been several D implementations of Signals and Slots.
9  * This version makes use of several new features in D, which make
10  * using it simpler and less error prone. In particular, it is no
11  * longer necessary to instrument the slots.
12  *
13  * References:
14  *      $(LUCKY A Deeper Look at Signals and Slots)$(BR)
15  *      $(LINK2 http://en.wikipedia.org/wiki/Observer_pattern, Observer pattern)$(BR)
16  *      $(LINK2 http://en.wikipedia.org/wiki/Signals_and_slots, Wikipedia)$(BR)
17  *      $(LINK2 http://boost.org/doc/html/$(SIGNALS).html, Boost Signals)$(BR)
18  *      $(LINK2 http://qt-project.org/doc/qt-5/signalsandslots.html, Qt)$(BR)
19  *
20  *      There has been a great deal of discussion in the D newsgroups
21  *      over this, and several implementations:
22  *
23  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/announce/signal_slots_library_4825.html, signal slots library)$(BR)
24  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Signals_and_Slots_in_D_42387.html, Signals and Slots in D)$(BR)
25  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Dynamic_binding_--_Qt_s_Signals_and_Slots_vs_Objective-C_42260.html, Dynamic binding -- Qt's Signals and Slots vs Objective-C)$(BR)
26  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/Dissecting_the_SS_42377.html, Dissecting the SS)$(BR)
27  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/dwt/about_harmonia_454.html, about harmonia)$(BR)
28  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/announce/1502.html, Another event handling module)$(BR)
29  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/41825.html, Suggestion: signal/slot mechanism)$(BR)
30  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/13251.html, Signals and slots?)$(BR)
31  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/10714.html, Signals and slots ready for evaluation)$(BR)
32  *      $(LINK2 http://www.digitalmars.com/d/archives/digitalmars/D/1393.html, Signals & Slots for Walter)$(BR)
33  *      $(LINK2 http://www.digitalmars.com/d/archives/28456.html, Signal/Slot mechanism?)$(BR)
34  *      $(LINK2 http://www.digitalmars.com/d/archives/19470.html, Modern Features?)$(BR)
35  *      $(LINK2 http://www.digitalmars.com/d/archives/16592.html, Delegates vs interfaces)$(BR)
36  *      $(LINK2 http://www.digitalmars.com/d/archives/16583.html, The importance of component programming (properties$(COMMA) signals and slots$(COMMA) etc))$(BR)
37  *      $(LINK2 http://www.digitalmars.com/d/archives/16368.html, signals and slots)$(BR)
38  *
39  * Bugs:
40  *      $(RED Slots can only be delegates formed from class objects or
41  *      interfaces to class objects. If a delegate to something else
42  *      is passed to connect(), such as a struct member function,
43  *      a nested function, a COM interface or a closure, undefined behavior
44  *      will result.)
45  *
46  *      Not safe for multiple threads operating on the same signals
47  *      or slots.
48  * Macros:
49  *      SIGNALS=signals
50  *
51  * Copyright: Copyright The D Language Foundation 2000 - 2009.
52  * License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
53  * Authors:   $(HTTP digitalmars.com, Walter Bright)
54  * Source:    $(PHOBOSSRC std/signals.d)
55  *
56  * $(SCRIPT inhibitQuickIndex = 1;)
57  */
58 /*          Copyright The D Language Foundation 2000 - 2009.
59  * Distributed under the Boost Software License, Version 1.0.
60  *    (See accompanying file LICENSE_1_0.txt or copy at
61  *          http://www.boost.org/LICENSE_1_0.txt)
62  */
63 module std.signals;
64 
65 import core.exception : onOutOfMemoryError;
66 import core.stdc.stdlib : calloc, realloc, free;
67 import std.stdio;
68 
69 // Special function for internal use only.
70 // Use of this is where the slot had better be a delegate
71 // to an object or an interface that is part of an object.
72 extern (C) Object _d_toObject(void* p);
73 
74 // Used in place of Object.notifyRegister and Object.notifyUnRegister.
75 alias DisposeEvt = void delegate(Object);
76 extern (C) void  rt_attachDisposeEvent( Object obj, DisposeEvt evt );
77 extern (C) void  rt_detachDisposeEvent( Object obj, DisposeEvt evt );
78 //debug=signal;
79 
80 /************************
81  * Mixin to create a signal within a class object.
82  *
83  * Different signals can be added to a class by naming the mixins.
84  */
85 
86 mixin template Signal(T1...)
87 {
88     static import core.exception;
89     static import core.stdc.stdlib;
90     /***
91      * A slot is implemented as a delegate.
92      * The slot_t is the type of the delegate.
93      * The delegate must be to an instance of a class or an interface
94      * to a class instance.
95      * Delegates to struct instances or nested functions must not be
96      * used as slots.
97      */
98     alias slot_t = void delegate(T1);
99 
100     /***
101      * Call each of the connected slots, passing the argument(s) i to them.
102      * Nested call will be ignored.
103      */
104     final void emit( T1 i )
105     {
106         if (status >= ST.inemitting || !slots.length)
107             return; // should not nest
108 
109         status = ST.inemitting;
110         scope (exit)
111             status = ST.idle;
112 
113         foreach (slot; slots[0 .. slots_idx])
114         {   if (slot)
115                 slot(i);
116         }
117 
118         assert(status >= ST.inemitting);
119         if (status == ST.inemitting_disconnected)
120         {
121             for (size_t j = 0; j < slots_idx;)
122             {
123                 if (slots[j] is null)
124                 {
125                     slots_idx--;
126                     slots[j] = slots[slots_idx];
127                 }
128                 else
129                     j++;
130             }
131         }
132     }
133 
134     /***
135      * Add a slot to the list of slots to be called when emit() is called.
136      */
137     final void connect(slot_t slot)
138     {
139         /* Do this:
140          *    slots ~= slot;
141          * but use malloc() and friends instead
142          */
143         auto len = slots.length;
144         if (slots_idx == len)
145         {
146             if (slots.length == 0)
147             {
148                 len = 4;
149                 auto p = core.stdc.stdlib.calloc(slot_t.sizeof, len);
150                 if (!p)
151                     core.exception.onOutOfMemoryError();
152                 slots = (cast(slot_t*) p)[0 .. len];
153             }
154             else
155             {
156                 import core.checkedint : addu, mulu;
157                 bool overflow;
158                 len = addu(mulu(len, 2, overflow), 4, overflow); // len = len * 2 + 4
159                 const nbytes = mulu(len, slot_t.sizeof, overflow);
160                 if (overflow) assert(0);
161 
162                 auto p = core.stdc.stdlib.realloc(slots.ptr, nbytes);
163                 if (!p)
164                     core.exception.onOutOfMemoryError();
165                 slots = (cast(slot_t*) p)[0 .. len];
166                 slots[slots_idx + 1 .. $] = null;
167             }
168         }
169         slots[slots_idx++] = slot;
170 
171      L1:
172         Object o = _d_toObject(slot.ptr);
173         rt_attachDisposeEvent(o, &unhook);
174     }
175 
176     /***
177      * Remove a slot from the list of slots to be called when emit() is called.
178      */
179     final void disconnect(slot_t slot)
180     {
181         debug (signal) writefln("Signal.disconnect(slot)");
182         size_t disconnectedSlots = 0;
183         size_t instancePreviousSlots = 0;
184         if (status >= ST.inemitting)
185         {
186             foreach (i, sloti; slots[0 .. slots_idx])
187             {
188                 if (sloti.ptr == slot.ptr &&
189                     ++instancePreviousSlots &&
190                     sloti == slot)
191                 {
192                     disconnectedSlots++;
193                     slots[i] = null;
194                     status = ST.inemitting_disconnected;
195                 }
196             }
197         }
198         else
199         {
200             for (size_t i = 0; i < slots_idx; )
201             {
202                 if (slots[i].ptr == slot.ptr &&
203                     ++instancePreviousSlots &&
204                     slots[i] == slot)
205                 {
206                     slots_idx--;
207                     disconnectedSlots++;
208                     slots[i] = slots[slots_idx];
209                     slots[slots_idx] = null;        // not strictly necessary
210                 }
211                 else
212                     i++;
213             }
214         }
215 
216          // detach object from dispose event if all its slots have been removed
217         if (instancePreviousSlots == disconnectedSlots)
218         {
219             Object o = _d_toObject(slot.ptr);
220             rt_detachDisposeEvent(o, &unhook);
221         }
222      }
223 
224     /***
225      * Disconnect all the slots.
226      */
227     final void disconnectAll()
228     {
229         debug (signal) writefln("Signal.disconnectAll");
230         __dtor();
231         slots_idx = 0;
232         status = ST.idle;
233     }
234 
235     /* **
236      * Special function called when o is destroyed.
237      * It causes any slots dependent on o to be removed from the list
238      * of slots to be called by emit().
239      */
240     final void unhook(Object o)
241     in { assert( status == ST.idle ); }
242     do
243     {
244         debug (signal) writefln("Signal.unhook(o = %s)", cast(void*) o);
245         for (size_t i = 0; i < slots_idx; )
246         {
247             if (_d_toObject(slots[i].ptr) is o)
248             {   slots_idx--;
249                 slots[i] = slots[slots_idx];
250                 slots[slots_idx] = null;        // not strictly necessary
251             }
252             else
253                 i++;
254         }
255     }
256 
257     /* **
258      * There can be multiple destructors inserted by mixins.
259      */
260     ~this()
261     {
262         /* **
263          * When this object is destroyed, need to let every slot
264          * know that this object is destroyed so they are not left
265          * with dangling references to it.
266          */
267         if (slots.length)
268         {
269             foreach (slot; slots[0 .. slots_idx])
270             {
271                 if (slot)
272                 {   Object o = _d_toObject(slot.ptr);
273                     rt_detachDisposeEvent(o, &unhook);
274                 }
275             }
276             core.stdc.stdlib.free(slots.ptr);
277             slots = null;
278         }
279     }
280 
281   private:
282     slot_t[] slots;             // the slots to call from emit()
283     size_t slots_idx;           // used length of slots[]
284 
285     enum ST { idle, inemitting, inemitting_disconnected }
286     ST status;
287 }
288 
289 ///
290 @system unittest
291 {
292     import std.signals;
293 
294     int observedMessageCounter = 0;
295 
296     class Observer
297     {   // our slot
298         void watch(string msg, int value)
299         {
300             switch (observedMessageCounter++)
301             {
302                 case 0:
303                     assert(msg == "setting new value");
304                     assert(value == 4);
305                     break;
306                 case 1:
307                     assert(msg == "setting new value");
308                     assert(value == 6);
309                     break;
310                 default:
311                     assert(0, "Unknown observation");
312             }
313         }
314     }
315 
316     class Observer2
317     {   // our slot
318         void watch(string msg, int value)
319         {
320         }
321     }
322 
323     class Foo
324     {
325         int value() { return _value; }
326 
327         int value(int v)
328         {
329             if (v != _value)
330             {   _value = v;
331                 // call all the connected slots with the two parameters
332                 emit("setting new value", v);
333             }
334             return v;
335         }
336 
337         // Mix in all the code we need to make Foo into a signal
338         mixin Signal!(string, int);
339 
340       private :
341         int _value;
342     }
343 
344     Foo a = new Foo;
345     Observer o = new Observer;
346     auto o2 = new Observer2;
347     auto o3 = new Observer2;
348     auto o4 = new Observer2;
349     auto o5 = new Observer2;
350 
351     a.value = 3;                // should not call o.watch()
352     a.connect(&o.watch);        // o.watch is the slot
353     a.connect(&o2.watch);
354     a.connect(&o3.watch);
355     a.connect(&o4.watch);
356     a.connect(&o5.watch);
357     a.value = 4;                // should call o.watch()
358     a.disconnect(&o.watch);     // o.watch is no longer a slot
359     a.disconnect(&o3.watch);
360     a.disconnect(&o5.watch);
361     a.disconnect(&o4.watch);
362     a.disconnect(&o2.watch);
363     a.value = 5;                // so should not call o.watch()
364     a.connect(&o2.watch);
365     a.connect(&o.watch);        // connect again
366     a.value = 6;                // should call o.watch()
367     destroy(o);                 // destroying o should automatically disconnect it
368     a.value = 7;                // should not call o.watch()
369 
370     assert(observedMessageCounter == 2);
371 }
372 
373 // A function whose sole purpose is to get this module linked in
374 // so the unittest will run.
375 void linkin() { }
376 
377 @system unittest
378 {
379     class Observer
380     {
381         void watch(string msg, int i)
382         {
383             //writefln("Observed msg '%s' and value %s", msg, i);
384             captured_value = i;
385             captured_msg   = msg;
386         }
387 
388         int    captured_value;
389         string captured_msg;
390     }
391 
392     class Foo
393     {
394         @property int value() { return _value; }
395 
396         @property int value(int v)
397         {
398             if (v != _value)
399             {   _value = v;
400                 emit("setting new value", v);
401             }
402             return v;
403         }
404 
405         mixin Signal!(string, int);
406 
407       private:
408         int _value;
409     }
410 
411     Foo a = new Foo;
412     Observer o = new Observer;
413 
414     // check initial condition
415     assert(o.captured_value == 0);
416     assert(o.captured_msg == "");
417 
418     // set a value while no observation is in place
419     a.value = 3;
420     assert(o.captured_value == 0);
421     assert(o.captured_msg == "");
422 
423     // connect the watcher and trigger it
424     a.connect(&o.watch);
425     a.value = 4;
426     assert(o.captured_value == 4);
427     assert(o.captured_msg == "setting new value");
428 
429     // disconnect the watcher and make sure it doesn't trigger
430     a.disconnect(&o.watch);
431     a.value = 5;
432     assert(o.captured_value == 4);
433     assert(o.captured_msg == "setting new value");
434 
435     // reconnect the watcher and make sure it triggers
436     a.connect(&o.watch);
437     a.value = 6;
438     assert(o.captured_value == 6);
439     assert(o.captured_msg == "setting new value");
440 
441     // destroy the underlying object and make sure it doesn't cause
442     // a crash or other problems
443     destroy(o);
444     a.value = 7;
445 }
446 
447 @system unittest
448 {
449     class Observer
450     {
451         int    i;
452         long   l;
453         string str;
454 
455         void watchInt(string str, int i)
456         {
457             this.str = str;
458             this.i = i;
459         }
460 
461         void watchLong(string str, long l)
462         {
463             this.str = str;
464             this.l = l;
465         }
466     }
467 
468     class Bar
469     {
470         @property void value1(int v)  { s1.emit("str1", v); }
471         @property void value2(int v)  { s2.emit("str2", v); }
472         @property void value3(long v) { s3.emit("str3", v); }
473 
474         mixin Signal!(string, int)  s1;
475         mixin Signal!(string, int)  s2;
476         mixin Signal!(string, long) s3;
477     }
478 
479     void test(T)(T a) {
480         auto o1 = new Observer;
481         auto o2 = new Observer;
482         auto o3 = new Observer;
483 
484         // connect the watcher and trigger it
485         a.s1.connect(&o1.watchInt);
486         a.s2.connect(&o2.watchInt);
487         a.s3.connect(&o3.watchLong);
488 
489         assert(!o1.i && !o1.l && o1.str == null);
490         assert(!o2.i && !o2.l && o2.str == null);
491         assert(!o3.i && !o3.l && o3.str == null);
492 
493         a.value1 = 11;
494         assert(o1.i == 11 && !o1.l && o1.str == "str1");
495         assert(!o2.i && !o2.l && o2.str == null);
496         assert(!o3.i && !o3.l && o3.str == null);
497         o1.i = -11; o1.str = "x1";
498 
499         a.value2 = 12;
500         assert(o1.i == -11 && !o1.l && o1.str == "x1");
501         assert(o2.i == 12 && !o2.l && o2.str == "str2");
502         assert(!o3.i && !o3.l && o3.str == null);
503         o2.i = -12; o2.str = "x2";
504 
505         a.value3 = 13;
506         assert(o1.i == -11 && !o1.l && o1.str == "x1");
507         assert(o2.i == -12 && !o1.l && o2.str == "x2");
508         assert(!o3.i && o3.l == 13 && o3.str == "str3");
509         o3.l = -13; o3.str = "x3";
510 
511         // disconnect the watchers and make sure it doesn't trigger
512         a.s1.disconnect(&o1.watchInt);
513         a.s2.disconnect(&o2.watchInt);
514         a.s3.disconnect(&o3.watchLong);
515 
516         a.value1 = 21;
517         a.value2 = 22;
518         a.value3 = 23;
519         assert(o1.i == -11 && !o1.l && o1.str == "x1");
520         assert(o2.i == -12 && !o1.l && o2.str == "x2");
521         assert(!o3.i && o3.l == -13 && o3.str == "x3");
522 
523         // reconnect the watcher and make sure it triggers
524         a.s1.connect(&o1.watchInt);
525         a.s2.connect(&o2.watchInt);
526         a.s3.connect(&o3.watchLong);
527 
528         a.value1 = 31;
529         a.value2 = 32;
530         a.value3 = 33;
531         assert(o1.i == 31 && !o1.l && o1.str == "str1");
532         assert(o2.i == 32 && !o1.l && o2.str == "str2");
533         assert(!o3.i && o3.l == 33 && o3.str == "str3");
534 
535         // destroy observers
536         destroy(o1);
537         destroy(o2);
538         destroy(o3);
539         a.value1 = 41;
540         a.value2 = 42;
541         a.value3 = 43;
542     }
543 
544     test(new Bar);
545 
546     class BarDerived: Bar
547     {
548         @property void value4(int v)  { s4.emit("str4", v); }
549         @property void value5(int v)  { s5.emit("str5", v); }
550         @property void value6(long v) { s6.emit("str6", v); }
551 
552         mixin Signal!(string, int)  s4;
553         mixin Signal!(string, int)  s5;
554         mixin Signal!(string, long) s6;
555     }
556 
557     auto a = new BarDerived;
558 
559     test!Bar(a);
560     test!BarDerived(a);
561 
562     auto o4 = new Observer;
563     auto o5 = new Observer;
564     auto o6 = new Observer;
565 
566     // connect the watcher and trigger it
567     a.s4.connect(&o4.watchInt);
568     a.s5.connect(&o5.watchInt);
569     a.s6.connect(&o6.watchLong);
570 
571     assert(!o4.i && !o4.l && o4.str == null);
572     assert(!o5.i && !o5.l && o5.str == null);
573     assert(!o6.i && !o6.l && o6.str == null);
574 
575     a.value4 = 44;
576     assert(o4.i == 44 && !o4.l && o4.str == "str4");
577     assert(!o5.i && !o5.l && o5.str == null);
578     assert(!o6.i && !o6.l && o6.str == null);
579     o4.i = -44; o4.str = "x4";
580 
581     a.value5 = 45;
582     assert(o4.i == -44 && !o4.l && o4.str == "x4");
583     assert(o5.i == 45 && !o5.l && o5.str == "str5");
584     assert(!o6.i && !o6.l && o6.str == null);
585     o5.i = -45; o5.str = "x5";
586 
587     a.value6 = 46;
588     assert(o4.i == -44 && !o4.l && o4.str == "x4");
589     assert(o5.i == -45 && !o4.l && o5.str == "x5");
590     assert(!o6.i && o6.l == 46 && o6.str == "str6");
591     o6.l = -46; o6.str = "x6";
592 
593     // disconnect the watchers and make sure it doesn't trigger
594     a.s4.disconnect(&o4.watchInt);
595     a.s5.disconnect(&o5.watchInt);
596     a.s6.disconnect(&o6.watchLong);
597 
598     a.value4 = 54;
599     a.value5 = 55;
600     a.value6 = 56;
601     assert(o4.i == -44 && !o4.l && o4.str == "x4");
602     assert(o5.i == -45 && !o4.l && o5.str == "x5");
603     assert(!o6.i && o6.l == -46 && o6.str == "x6");
604 
605     // reconnect the watcher and make sure it triggers
606     a.s4.connect(&o4.watchInt);
607     a.s5.connect(&o5.watchInt);
608     a.s6.connect(&o6.watchLong);
609 
610     a.value4 = 64;
611     a.value5 = 65;
612     a.value6 = 66;
613     assert(o4.i == 64 && !o4.l && o4.str == "str4");
614     assert(o5.i == 65 && !o4.l && o5.str == "str5");
615     assert(!o6.i && o6.l == 66 && o6.str == "str6");
616 
617     // destroy observers
618     destroy(o4);
619     destroy(o5);
620     destroy(o6);
621     a.value4 = 44;
622     a.value5 = 45;
623     a.value6 = 46;
624 }
625 
626 // Triggers bug from issue 15341
627 @system unittest
628 {
629     class Observer
630     {
631        void watch() { }
632        void watch2() { }
633     }
634 
635     class Bar
636     {
637        mixin Signal!();
638     }
639 
640    auto a = new Bar;
641    auto o = new Observer;
642 
643    //Connect both observer methods for the same instance
644    a.connect(&o.watch);
645    a.connect(&o.watch2); // not connecting watch2() or disconnecting it manually fixes the issue
646 
647    //Disconnect a single method of the two
648    a.disconnect(&o.watch); // NOT disconnecting watch() fixes the issue
649 
650    destroy(o); // destroying o should automatically call unhook and disconnect the slot for watch2
651    a.emit(); // should not raise segfault since &o.watch2 is no longer connected
652 }
653 
654 version (none) // Disabled because of https://issues.dlang.org/show_bug.cgi?id=5028
655 @system unittest
656 {
657     class A
658     {
659         mixin Signal!(string, int) s1;
660     }
661 
662     class B : A
663     {
664         mixin Signal!(string, int) s2;
665     }
666 }
667 
668 // Triggers bug from issue 16249
669 @system unittest
670 {
671     class myLINE
672     {
673         mixin Signal!( myLINE, int );
674 
675         void value( int v )
676         {
677             if ( v >= 0 ) emit( this, v );
678             else          emit( new myLINE, v );
679         }
680     }
681 
682     class Dot
683     {
684         int value;
685 
686         myLINE line_;
687         void line( myLINE line_x )
688         {
689             if ( line_ is line_x ) return;
690 
691             if ( line_ !is null )
692             {
693                 line_.disconnect( &watch );
694             }
695             line_ = line_x;
696             line_.connect( &watch );
697         }
698 
699         void watch( myLINE line_x, int value_x )
700         {
701             line = line_x;
702             value = value_x;
703         }
704     }
705 
706     auto dot1 = new Dot;
707     auto dot2 = new Dot;
708     auto line = new myLINE;
709     dot1.line = line;
710     dot2.line = line;
711 
712     line.value = 11;
713     assert( dot1.value == 11 );
714     assert( dot2.value == 11 );
715 
716     line.value = -22;
717     assert( dot1.value == -22 );
718     assert( dot2.value == -22 );
719 }
720 
721 @system unittest
722 {
723     import std.signals;
724 
725     class Observer
726     {   // our slot
727         void watch(string msg, int value)
728         {
729             if (value != 0)
730             {
731                 assert(msg == "setting new value");
732                 assert(value == 1);
733             }
734         }
735     }
736 
737     class Foo
738     {
739         int value() { return _value; }
740 
741         int value(int v)
742         {
743             if (v != _value)
744             {
745                 _value = v;
746                 // call all the connected slots with the parameters
747                 emit("setting new value", v);
748             }
749             return v;
750         }
751 
752         // Mix in all the code we need to make Foo into a signal
753         mixin Signal!(string, int);
754 
755       private :
756         int _value;
757     }
758 
759     Foo a = new Foo;
760     Observer o = new Observer;
761     auto o2 = new Observer;
762 
763     a.value = 3;                // should not call o.watch()
764     a.connect(&o.watch);        // o.watch is the slot
765     a.connect(&o2.watch);
766     a.value = 1;                // should call o.watch()
767     a.disconnectAll();
768     a.value = 5;                // so should not call o.watch()
769     a.connect(&o.watch);        // connect again
770     a.connect(&o2.watch);
771     a.value = 1;                // should call o.watch()
772     destroy(o);                 // destroying o should automatically disconnect it
773     destroy(o2);
774     a.value = 7;                // should not call o.watch()
775 }
Suggestion Box / Bug Report