summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarco Trevisan (TreviƱo) <mail@3v1n0.net>2022-06-09 04:31:30 +0200
committerPhilip Chimento <philip.chimento@gmail.com>2023-02-19 22:55:31 -0800
commit2e5fe3d968e9073302c8600e366ee8d2480bef73 (patch)
tree82fc22c1213bd1a2a167321369e45fbd00ac1a3c
parent92f7a45d5201d7d938116e8d450e50f2d93a88ae (diff)
downloadgjs-2e5fe3d968e9073302c8600e366ee8d2480bef73.tar.gz
signals: Simulate GObject's connect_after behavior on signals
GObject signals have a connect_after function that we don't have in the gjs core signals, while it can be useful in some situations. So introduce it.
-rw-r--r--installed-tests/js/testSignals.js264
-rw-r--r--modules/core/_signals.js30
-rw-r--r--modules/script/signals.js6
3 files changed, 183 insertions, 117 deletions
diff --git a/installed-tests/js/testSignals.js b/installed-tests/js/testSignals.js
index 1698abe6..b3684546 100644
--- a/installed-tests/js/testSignals.js
+++ b/installed-tests/js/testSignals.js
@@ -34,125 +34,161 @@ function testSignals(klass) {
expect(() => foo.emit('random-event')).not.toThrow();
});
- it('calls a signal handler when a signal is emitted', function () {
- foo.connect('bar', bar);
- foo.emit('bar', 'This is a', 'This is b');
- expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
- });
-
- it('calls remaining handlers after one is disconnected', function () {
- const id1 = foo.connect('bar', bar);
-
- const bar2 = jasmine.createSpy('bar2');
- const id2 = foo.connect('bar', bar2);
-
- foo.emit('bar');
- expect(bar).toHaveBeenCalledTimes(1);
- expect(bar2).toHaveBeenCalledTimes(1);
-
- foo.disconnect(id1);
-
- foo.emit('bar');
-
- expect(bar).toHaveBeenCalledTimes(1);
- expect(bar2).toHaveBeenCalledTimes(2);
-
- foo.disconnect(id2);
- });
-
- it('does not call a signal handler after the signal is disconnected', function () {
- let id = foo.connect('bar', bar);
- foo.emit('bar', 'This is a', 'This is b');
- bar.calls.reset();
- foo.disconnect(id);
- // this emission should do nothing
- foo.emit('bar', 'Another a', 'Another b');
- expect(bar).not.toHaveBeenCalled();
- });
-
- it('can disconnect a signal handler during signal emission', function () {
- var toRemove = [];
- let firstId = foo.connect('bar', function (theFoo) {
- theFoo.disconnect(toRemove[0]);
- theFoo.disconnect(toRemove[1]);
+ ['connect', 'connectAfter'].forEach(connectMethod => {
+ describe(`using ${connectMethod}`, function () {
+ it('calls a signal handler when a signal is emitted', function () {
+ foo[connectMethod]('bar', bar);
+ foo.emit('bar', 'This is a', 'This is b');
+ expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
+ });
+
+ it('calls remaining handlers after one is disconnected', function () {
+ const id1 = foo[connectMethod]('bar', bar);
+
+ const bar2 = jasmine.createSpy('bar2');
+ const id2 = foo[connectMethod]('bar', bar2);
+
+ foo.emit('bar');
+ expect(bar).toHaveBeenCalledTimes(1);
+ expect(bar2).toHaveBeenCalledTimes(1);
+
+ foo.disconnect(id1);
+
+ foo.emit('bar');
+
+ expect(bar).toHaveBeenCalledTimes(1);
+ expect(bar2).toHaveBeenCalledTimes(2);
+
+ foo.disconnect(id2);
+ });
+
+ it('does not call a signal handler after the signal is disconnected', function () {
+ let id = foo[connectMethod]('bar', bar);
+ foo.emit('bar', 'This is a', 'This is b');
+ bar.calls.reset();
+ foo.disconnect(id);
+ // this emission should do nothing
+ foo.emit('bar', 'Another a', 'Another b');
+ expect(bar).not.toHaveBeenCalled();
+ });
+
+ it('can disconnect a signal handler during signal emission', function () {
+ var toRemove = [];
+ let firstId = foo[connectMethod]('bar', function (theFoo) {
+ theFoo.disconnect(toRemove[0]);
+ theFoo.disconnect(toRemove[1]);
+ });
+ toRemove.push(foo[connectMethod]('bar', bar));
+ toRemove.push(foo[connectMethod]('bar', bar));
+
+ // emit signal; what should happen is that the second two handlers are
+ // disconnected before they get invoked
+ foo.emit('bar');
+ expect(bar).not.toHaveBeenCalled();
+
+ // clean up the last handler
+ foo.disconnect(firstId);
+
+ expect(() => foo.disconnect(firstId)).toThrowError(
+ `No signal connection ${firstId} found`);
+
+ // poke in private implementation to sanity-check no handlers left
+ expect(Object.keys(foo._signalConnections).length).toEqual(0);
+ });
+
+ it('distinguishes multiple signals', function () {
+ let bonk = jasmine.createSpy('bonk');
+ foo[connectMethod]('bar', bar);
+ foo[connectMethod]('bonk', bonk);
+ foo[connectMethod]('bar', bar);
+
+ foo.emit('bar');
+ expect(bar).toHaveBeenCalledTimes(2);
+ expect(bonk).not.toHaveBeenCalled();
+
+ foo.emit('bonk');
+ expect(bar).toHaveBeenCalledTimes(2);
+ expect(bonk).toHaveBeenCalledTimes(1);
+
+ foo.emit('bar');
+ expect(bar).toHaveBeenCalledTimes(4);
+ expect(bonk).toHaveBeenCalledTimes(1);
+
+ foo.disconnectAll();
+ bar.calls.reset();
+ bonk.calls.reset();
+
+ // these post-disconnect emissions should do nothing
+ foo.emit('bar');
+ foo.emit('bonk');
+ expect(bar).not.toHaveBeenCalled();
+ expect(bonk).not.toHaveBeenCalled();
+ });
+
+ it('determines if a signal is connected on a JS object', function () {
+ let id = foo[connectMethod]('bar', bar);
+ expect(foo.signalHandlerIsConnected(id)).toEqual(true);
+ foo.disconnect(id);
+ expect(foo.signalHandlerIsConnected(id)).toEqual(false);
+ });
+
+ it('does not call a subsequent connected callbacks if stopped by earlier', function () {
+ const afterBar = jasmine.createSpy('bar');
+ const afterAfterBar = jasmine.createSpy('barBar');
+ foo[connectMethod]('bar', bar.and.returnValue(true));
+ foo[connectMethod]('bar', afterBar);
+ foo[connectMethod]('bar', afterAfterBar);
+ foo.emit('bar', 'This is a', 123);
+ expect(bar).toHaveBeenCalledWith(foo, 'This is a', 123);
+ expect(afterBar).not.toHaveBeenCalled();
+ expect(afterAfterBar).not.toHaveBeenCalled();
+ });
+
+ describe('with exception in signal handler', function () {
+ let bar2;
+ beforeEach(function () {
+ bar.and.throwError('Exception we are throwing on purpose');
+ bar2 = jasmine.createSpy('bar');
+ foo[connectMethod]('bar', bar);
+ foo[connectMethod]('bar', bar2);
+ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+ 'JS ERROR: Exception in callback for signal: *');
+ foo.emit('bar');
+ });
+
+ it('does not affect other callbacks', function () {
+ expect(bar).toHaveBeenCalledTimes(1);
+ expect(bar2).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not disconnect the callback', function () {
+ GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+ 'JS ERROR: Exception in callback for signal: *');
+ foo.emit('bar');
+ expect(bar).toHaveBeenCalledTimes(2);
+ expect(bar2).toHaveBeenCalledTimes(2);
+ });
+ });
});
- toRemove.push(foo.connect('bar', bar));
- toRemove.push(foo.connect('bar', bar));
-
- // emit signal; what should happen is that the second two handlers are
- // disconnected before they get invoked
- foo.emit('bar');
- expect(bar).not.toHaveBeenCalled();
-
- // clean up the last handler
- foo.disconnect(firstId);
-
- expect(() => foo.disconnect(firstId)).toThrowError(
- `No signal connection ${firstId} found`);
-
- // poke in private implementation to sanity-check no handlers left
- expect(Object.keys(foo._signalConnections).length).toEqual(0);
});
- it('distinguishes multiple signals', function () {
- let bonk = jasmine.createSpy('bonk');
- foo.connect('bar', bar);
- foo.connect('bonk', bonk);
+ it('using connectAfter calls a signal handler later than when using connect when a signal is emitted', function () {
+ const afterBar = jasmine.createSpy('bar');
+ foo.connectAfter('bar', (...args) => {
+ expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
+ afterBar(...args);
+ });
foo.connect('bar', bar);
-
- foo.emit('bar');
- expect(bar).toHaveBeenCalledTimes(2);
- expect(bonk).not.toHaveBeenCalled();
-
- foo.emit('bonk');
- expect(bar).toHaveBeenCalledTimes(2);
- expect(bonk).toHaveBeenCalledTimes(1);
-
- foo.emit('bar');
- expect(bar).toHaveBeenCalledTimes(4);
- expect(bonk).toHaveBeenCalledTimes(1);
-
- foo.disconnectAll();
- bar.calls.reset();
- bonk.calls.reset();
-
- // these post-disconnect emissions should do nothing
- foo.emit('bar');
- foo.emit('bonk');
- expect(bar).not.toHaveBeenCalled();
- expect(bonk).not.toHaveBeenCalled();
- });
-
- it('determines if a signal is connected on a JS object', function () {
- let id = foo.connect('bar', bar);
- expect(foo.signalHandlerIsConnected(id)).toEqual(true);
- foo.disconnect(id);
- expect(foo.signalHandlerIsConnected(id)).toEqual(false);
+ foo.emit('bar', 'This is a', 'This is b');
+ expect(afterBar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
});
- describe('with exception in signal handler', function () {
- let bar2;
- beforeEach(function () {
- bar.and.throwError('Exception we are throwing on purpose');
- bar2 = jasmine.createSpy('bar');
- foo.connect('bar', bar);
- foo.connect('bar', bar2);
- GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
- 'JS ERROR: Exception in callback for signal: *');
- foo.emit('bar');
- });
-
- it('does not affect other callbacks', function () {
- expect(bar).toHaveBeenCalledTimes(1);
- expect(bar2).toHaveBeenCalledTimes(1);
- });
-
- it('does not disconnect the callback', function () {
- GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
- 'JS ERROR: Exception in callback for signal: *');
- foo.emit('bar');
- expect(bar).toHaveBeenCalledTimes(2);
- expect(bar2).toHaveBeenCalledTimes(2);
- });
+ it('does not call a connected after handler when stopped by connect', function () {
+ const afterBar = jasmine.createSpy('bar');
+ foo.connectAfter('bar', afterBar);
+ foo.connect('bar', bar.and.returnValue(true));
+ foo.emit('bar', 'This is a', 'This is b');
+ expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
+ expect(afterBar).not.toHaveBeenCalled();
});
}
diff --git a/modules/core/_signals.js b/modules/core/_signals.js
index 7dabdca3..0925a45c 100644
--- a/modules/core/_signals.js
+++ b/modules/core/_signals.js
@@ -11,7 +11,7 @@
// 3) the expectation is that a given object will have a very small number of
// connections, but they may be to different signal names
-function _connect(name, callback) {
+function _connectFull(name, callback, after) {
// be paranoid about callback arg since we'd start to throw from emit()
// if it was messed up
if (typeof callback !== 'function')
@@ -31,6 +31,7 @@ function _connect(name, callback) {
this._signalConnections[id] = {
name,
callback,
+ after,
};
const connectionsByName = this._signalConnectionsByName[name] ?? [];
@@ -42,6 +43,14 @@ function _connect(name, callback) {
return id;
}
+function _connect(name, callback) {
+ return _connectFull.call(this, name, callback, false);
+}
+
+function _connectAfter(name, callback) {
+ return _connectFull.call(this, name, callback, true);
+}
+
function _disconnect(id) {
const connection = this._signalConnections?.[id];
@@ -98,6 +107,20 @@ function _emit(name, ...args) {
// which would be a cycle.
const argArray = [this, ...args];
+ const afterHandlers = [];
+ const beforeHandlers = handlers.filter(c => {
+ if (!c.after)
+ return true;
+
+ afterHandlers.push(c);
+ return false;
+ });
+
+ if (!_callHandlers(beforeHandlers, argArray))
+ _callHandlers(afterHandlers, argArray);
+}
+
+function _callHandlers(handlers, argArray) {
for (const handler of handlers) {
if (handler.disconnected)
continue;
@@ -109,13 +132,15 @@ function _emit(name, ...args) {
// if the callback returns true, we don't call the next
// signal handlers
if (ret === true)
- break;
+ return true;
} catch (e) {
// just log any exceptions so that callbacks can't disrupt
// signal emission
logError(e, `Exception in callback for signal: ${handler.name}`);
}
}
+
+ return false;
}
function _addSignalMethod(proto, functionName, func) {
@@ -127,6 +152,7 @@ function _addSignalMethod(proto, functionName, func) {
function addSignalMethods(proto) {
_addSignalMethod(proto, 'connect', _connect);
+ _addSignalMethod(proto, 'connectAfter', _connectAfter);
_addSignalMethod(proto, 'disconnect', _disconnect);
_addSignalMethod(proto, 'emit', _emit);
_addSignalMethod(proto, 'signalHandlerIsConnected', _signalHandlerIsConnected);
diff --git a/modules/script/signals.js b/modules/script/signals.js
index cd10605c..f0bc6926 100644
--- a/modules/script/signals.js
+++ b/modules/script/signals.js
@@ -6,7 +6,10 @@
const Lang = imports.lang;
// Private API, remains exported for backwards compatibility reasons
-var {_connect, _disconnect, _emit, _signalHandlerIsConnected, _disconnectAll} = imports._signals;
+var {
+ _connect, _connectAfter, _disconnect, _emit, _signalHandlerIsConnected,
+ _disconnectAll,
+} = imports._signals;
// Public API
var {addSignalMethods} = imports._signals;
@@ -14,6 +17,7 @@ var {addSignalMethods} = imports._signals;
var WithSignals = new Lang.Interface({
Name: 'WithSignals',
connect: _connect,
+ connectAfter: _connectAfter,
disconnect: _disconnect,
emit: _emit,
signalHandlerIsConnected: _signalHandlerIsConnected,