Branch data Line data Source code
1 : 22 : // SPDX-License-Identifier: MIT OR LGPL-2.0-or-later
2 : : // SPDX-FileCopyrightText: 2008 litl, LLC
3 : : // SPDX-FileCopyrightText: 2022 Canonical Ltd.
4 : : // SPDX-FileContributor: Marco Trevisan <marco.trevisan@canonical.com>
5 : :
6 : : /* exported addSignalMethods */
7 : :
8 : : // A couple principals of this simple signal system:
9 : : // 1) should look just like our GObject signal binding
10 : : // 2) memory and safety matter more than speed of connect/disconnect/emit
11 : : // 3) the expectation is that a given object will have a very small number of
12 : : // connections, but they may be to different signal names
13 : :
14 : 84 : function _connectFull(name, callback, after) {
15 : : // be paranoid about callback arg since we'd start to throw from emit()
16 : : // if it was messed up
17 [ + - ]: 84 : if (typeof callback !== 'function')
18 : 0 : throw new Error('When connecting signal must give a callback that is a function');
19 : :
20 : : // we instantiate the "signal machinery" only on-demand if anything
21 : : // gets connected.
22 [ + + ]: 84 : if (this._signalConnections === undefined) {
23 : 42 : this._signalConnections = Object.create(null);
24 : 42 : this._signalConnectionsByName = Object.create(null);
25 : 42 : this._nextConnectionId = 1;
26 : : }
27 : :
28 : 84 : const id = this._nextConnectionId;
29 : 84 : this._nextConnectionId += 1;
30 : :
31 : 84 : this._signalConnections[id] = {
32 : 84 : name,
33 : 84 : callback,
34 : 84 : after,
35 : : };
36 : :
37 [ + + ]: 84 : const connectionsByName = this._signalConnectionsByName[name] ?? [];
38 : :
39 [ + + ]: 84 : if (!connectionsByName.length)
40 : 48 : this._signalConnectionsByName[name] = connectionsByName;
41 : 84 : connectionsByName.push(id);
42 : :
43 : 84 : return id;
44 : 84 : }
45 : :
46 : 44 : function _connect(name, callback) {
47 : 44 : return _connectFull.call(this, name, callback, false);
48 : : }
49 : :
50 : 40 : function _connectAfter(name, callback) {
51 : 40 : return _connectFull.call(this, name, callback, true);
52 : : }
53 : :
54 : 35 : function _disconnect(id) {
55 [ - + ]: 35 : const connection = this._signalConnections?.[id];
56 : :
57 [ + + ]: 35 : if (!connection)
58 : 4 : throw new Error(`No signal connection ${id} found`);
59 : :
60 [ + - ]: 31 : if (connection.disconnected)
61 : 0 : throw new Error(`Signal handler id ${id} already disconnected`);
62 : :
63 : 31 : connection.disconnected = true;
64 : 31 : delete this._signalConnections[id];
65 : :
66 : 31 : const ids = this._signalConnectionsByName[connection.name];
67 [ + - ]: 31 : if (!ids)
68 : 0 : return;
69 : :
70 : 31 : const indexOfId = ids.indexOf(id);
71 [ - + ]: 31 : if (indexOfId !== -1)
72 : 31 : ids.splice(indexOfId, 1);
73 : :
74 [ + + ]: 31 : if (ids.length === 0)
75 : 19 : delete this._signalConnectionsByName[connection.name];
76 : 0 : }
77 : :
78 : 8 : function _signalHandlerIsConnected(id) {
79 [ - + ]: 8 : const connection = this._signalConnections?.[id];
80 [ + + ]: 8 : return !!connection && !connection.disconnected;
81 : 8 : }
82 : :
83 : 4 : function _disconnectAll() {
84 [ + - ]: 16 : Object.values(this._signalConnections ?? {}).forEach(c => (c.disconnected = true));
85 : 4 : delete this._signalConnections;
86 : 4 : delete this._signalConnectionsByName;
87 : : }
88 : :
89 : 380 : function _emit(name, ...args) {
90 [ + + ]: 380 : const connections = this._signalConnectionsByName?.[name];
91 : :
92 : : // may not be any signal handlers at all, if not then return
93 [ + + ]: 380 : if (!connections)
94 : 18 : return;
95 : :
96 : : // To deal with re-entrancy (removal/addition while
97 : : // emitting), we copy out a list of what was connected
98 : : // at emission start; and just before invoking each
99 : : // handler we check its disconnected flag.
100 : 768 : const handlers = connections.map(id => this._signalConnections[id]);
101 : :
102 : : // create arg array which is emitter + everything passed in except
103 : : // signal name. Would be more convenient not to pass emitter to
104 : : // the callback, but trying to be 100% consistent with GObject
105 : : // which does pass it in. Also if we pass in the emitter here,
106 : : // people don't create closures with the emitter in them,
107 : : // which would be a cycle.
108 [ + + ]: 362 : const argArray = [this, ...args];
109 : :
110 : 362 : const afterHandlers = [];
111 : 362 : const beforeHandlers = handlers.filter(c => {
112 [ + + ]: 406 : if (!c.after)
113 : 358 : return true;
114 : :
115 : 48 : afterHandlers.push(c);
116 : 48 : return false;
117 : : });
118 : :
119 [ + + ]: 362 : if (!_callHandlers(beforeHandlers, argArray))
120 : 50 : _callHandlers(afterHandlers, argArray);
121 : 18 : }
122 : :
123 : 412 : function _callHandlers(handlers, argArray) {
124 [ + + ]: 494 : for (const handler of handlers) {
125 [ + + ]: 396 : if (handler.disconnected)
126 : : continue;
127 : :
128 : 388 : try {
129 : : // since we pass "null" for this, the global object will be used.
130 : 388 : const ret = handler.callback.apply(null, argArray);
131 : :
132 : : // if the callback returns true, we don't call the next
133 : : // signal handlers
134 [ + + ]: 376 : if (ret === true)
135 : 314 : return true;
136 : 12 : } catch (e) {
137 : : // just log any exceptions so that callbacks can't disrupt
138 : : // signal emission
139 : 12 : logError(e, `Exception in callback for signal: ${handler.name}`);
140 : : }
141 : : }
142 : :
143 : 98 : return false;
144 : 314 : }
145 : :
146 : 18 : function _addSignalMethod(proto, functionName, func) {
147 [ + - ][ + - ]: 18 : if (proto[functionName] && proto[functionName] !== func)
148 : 0 : log(`WARNING: addSignalMethods is replacing existing ${proto} ${functionName} method`);
149 : :
150 : 18 : proto[functionName] = func;
151 : : }
152 : :
153 : 3 : function addSignalMethods(proto) {
154 : 3 : _addSignalMethod(proto, 'connect', _connect);
155 : 3 : _addSignalMethod(proto, 'connectAfter', _connectAfter);
156 : 3 : _addSignalMethod(proto, 'disconnect', _disconnect);
157 : 3 : _addSignalMethod(proto, 'emit', _emit);
158 : 3 : _addSignalMethod(proto, 'signalHandlerIsConnected', _signalHandlerIsConnected);
159 : : // this one is not in GObject, but useful
160 : 3 : _addSignalMethod(proto, 'disconnectAll', _disconnectAll);
161 : : }
|