diff options
author | Marco Trevisan (TreviƱo) <mail@3v1n0.net> | 2022-06-09 04:31:30 +0200 |
---|---|---|
committer | Philip Chimento <philip.chimento@gmail.com> | 2023-02-19 22:55:31 -0800 |
commit | 2e5fe3d968e9073302c8600e366ee8d2480bef73 (patch) | |
tree | 82fc22c1213bd1a2a167321369e45fbd00ac1a3c | |
parent | 92f7a45d5201d7d938116e8d450e50f2d93a88ae (diff) | |
download | gjs-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.js | 264 | ||||
-rw-r--r-- | modules/core/_signals.js | 30 | ||||
-rw-r--r-- | modules/script/signals.js | 6 |
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, |