mini-signals
Version:
signals, in TypeScript, fast
541 lines (540 loc) • 21 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const mini_signals_1 = require("../src/mini-signals");
const chai_1 = require("chai");
describe('MiniSignal', () => {
const pattern = [];
const writer = (a) => {
pattern.push(a);
};
beforeEach(() => {
pattern.length = 0;
});
it('quick test', () => {
const e = new mini_signals_1.MiniSignal();
const foo = e.add(writer);
e.add(writer);
const bar = e.add(writer);
(0, chai_1.expect)(e instanceof mini_signals_1.MiniSignal);
e.dispatch('banana');
e.dispatch('apple');
e.detach(foo);
e.detach(bar);
e.dispatch('pear');
e.detachAll();
e.dispatch('raspberry');
(0, chai_1.expect)(pattern.join(';')).to.equal('banana;banana;banana;apple;apple;apple;pear');
});
describe('Readme Examples', () => {
it('Example Usage', () => {
const mySignal = new mini_signals_1.MiniSignal();
const binding = mySignal.add(onSignal); // add listener
mySignal.dispatch('foo', 'bar'); // dispatch signal passing custom parameters
mySignal.detach(binding); // remove a single listener
function onSignal(foo, bar) {
(0, chai_1.expect)(foo).to.equal('foo');
(0, chai_1.expect)(bar).to.equal('bar');
}
});
it('Function#bind example', () => {
const mySignal = new mini_signals_1.MiniSignal();
const context = {};
const cb = function (bar) {
(0, chai_1.expect)(arguments).has.length(1);
(0, chai_1.expect)(bar).equals('bar');
(0, chai_1.expect)(this).equals(context);
}.bind(context);
mySignal.add(cb);
mySignal.dispatch('bar');
});
it('Function#bind example with parameters', () => {
const mySignal = new mini_signals_1.MiniSignal();
const context = {};
const cb = function (bar) {
(0, chai_1.expect)(arguments).has.length(1);
(0, chai_1.expect)(bar).equals('bar');
(0, chai_1.expect)(this).equals(context);
}.bind(context, 'bar');
mySignal.add(cb);
mySignal.dispatch();
});
});
describe('MiniSignal#add', () => {
let e;
beforeEach(() => {
e = new mini_signals_1.MiniSignal();
});
it('should throw error for incorrect types', () => {
(0, chai_1.expect)(() => {
// @ts-expect-error testing error
e.add();
}).throws('MiniSignal#add(): First arg must be a Function.');
(0, chai_1.expect)(() => {
// @ts-expect-error testing error
e.add(123);
}).throws('MiniSignal#add(): First arg must be a Function.');
(0, chai_1.expect)(() => {
// @ts-expect-error testing error
e.add(true);
}).throws('MiniSignal#add(): First arg must be a Function.');
(0, chai_1.expect)(!e.hasListeners);
});
// Note: once is deprecated
// These tests use the add method instead
it('should not invoke twice', () => {
const l = e.add(function (arg) {
writer(arg);
(0, chai_1.expect)(arg).equals('foo');
e.detach(l);
});
e.dispatch('foo');
e.dispatch('bar');
e.dispatch('baz');
(0, chai_1.expect)(pattern.join(';')).equals('foo');
});
});
describe('MiniSignal#dispatch', () => {
let e;
beforeEach(() => {
e = new mini_signals_1.MiniSignal();
});
it('should return false when there are not events to dispatch', () => {
(0, chai_1.expect)(e.dispatch('foo')).equals(false);
(0, chai_1.expect)(e.dispatch('bar')).equals(false);
});
it('emits with context when function is bound function', () => {
const cb = function (bar) {
(0, chai_1.expect)(bar).equals('bar');
(0, chai_1.expect)(this).equals(context);
(0, chai_1.expect)(arguments).has.length(1);
}.bind(context);
e.add(cb);
e.dispatch('bar');
});
it('can dispatch the function with multiple arguments', () => {
for (let i = 0; i < 100; i++) {
const e = new mini_signals_1.MiniSignal();
(function (j) {
const args = [];
for (let i = 0; i < j; i++) {
args.push(j);
}
e.add((..._args) => {
(0, chai_1.expect)(_args.length).equals(args.length);
});
e.dispatch.apply(e, args);
})(i);
}
});
it('can dispatch the function with multiple arguments, multiple listeners', () => {
for (let i = 0; i < 100; i++) {
const e = new mini_signals_1.MiniSignal();
(function (j) {
const args = [];
for (let i = 0; i < j; i++) {
args.push(j);
}
e.add((..._args) => {
(0, chai_1.expect)(_args.length).equals(args.length);
});
e.add((..._args) => {
(0, chai_1.expect)(_args.length).equals(args.length);
});
e.add((..._args) => {
(0, chai_1.expect)(_args.length).equals(args.length);
});
e.add((..._args) => {
(0, chai_1.expect)(_args.length).equals(args.length);
});
e.dispatch.apply(e, args);
})(i);
}
});
it('can dispatch many listeners', () => {
const N = 10000;
let sum = 0;
function add(i) {
sum += i;
}
for (let i = 0; i <= N; i++) {
e.add(add.bind(this, i));
}
e.dispatch();
(0, chai_1.expect)(sum).equals((N * (N + 1)) / 2);
});
it('should return true when there are events to dispatch', function (done) {
e.add(() => {
process.nextTick(done);
});
(0, chai_1.expect)(e.dispatch()).equals(true);
});
it('should return false when there are no events to dispatch', () => {
(0, chai_1.expect)(e.dispatch()).equals(false);
});
it('receives the emitted events', (done) => {
const e = new mini_signals_1.MiniSignal();
e.add(function (a, b, c, undef) {
(0, chai_1.expect)(a).equals('foo');
(0, chai_1.expect)(b).equals(e);
(0, chai_1.expect)(c).is.instanceOf(Date);
(0, chai_1.expect)(undef).equals(undefined);
(0, chai_1.expect)(arguments).has.length(3);
done();
});
e.dispatch('foo', e, new Date());
});
it('emits to all event listeners', () => {
const e = new mini_signals_1.MiniSignal();
const pattern = [];
e.add(() => {
pattern.push('foo1');
});
e.add(() => {
pattern.push('foo2');
});
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('foo1;foo2');
});
it('emits to all event listeners', () => {
const e = new mini_signals_1.MiniSignal();
function foo1() {
pattern.push('foo1');
}
function foo2() {
pattern.push('foo2');
}
function foo3() {
pattern.push('foo3');
}
e.add(foo1);
e.add(foo2);
e.add(foo3);
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('foo1;foo2;foo3');
});
it('emits to all event listeners, removes once', () => {
const e = new mini_signals_1.MiniSignal();
e.add(() => {
pattern.push('foo1');
});
const l = e.add(() => {
pattern.push('foo2');
e.detach(l);
});
e.add(() => {
pattern.push('foo3');
});
e.dispatch();
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('foo1;foo2;foo3;foo1;foo3');
});
it('cannot dispatch while dispatching', () => {
const cb = function (bar) {
(0, chai_1.expect)(bar).equals('bar');
(0, chai_1.expect)(arguments).has.length(1);
e.dispatch('bar');
};
e.add(cb);
(0, chai_1.expect)(() => {
e.dispatch('bar');
}).throws('MiniSignal#dispatch(): Signal already dispatching.');
});
});
describe('MiniSignal#detach', () => {
/* istanbul ignore next */
function foo() {
pattern.push('foo');
}
/* istanbul ignore next */
function bar() {
pattern.push('bar');
}
/* istanbul ignore next */
function a() {
pattern.push('a');
}
/* istanbul ignore next */
function b() {
pattern.push('b');
}
let e;
let pattern = [];
beforeEach(() => {
e = new mini_signals_1.MiniSignal();
pattern = [];
});
it('should throw an error if not a SignalBinding', () => {
(0, chai_1.expect)(() => {
// @ts-expect-error testing error
e.detach();
}).throws('MiniSignal#detach(): First arg must be a MiniSignal listener reference.');
(0, chai_1.expect)(() => {
// @ts-expect-error testing error
e.detach(1);
}).throws('MiniSignal#detach(): First arg must be a MiniSignal listener reference.');
(0, chai_1.expect)(() => {
// @ts-expect-error testing error
e.detach(bar);
}).throws('MiniSignal#detach(): First arg must be a MiniSignal listener reference.');
});
it('should only remove the event with the specified node', () => {
e.add(a);
e.add(b);
const _bar = e.add(bar);
(0, chai_1.expect)(e.hasListeners()).equals(true);
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('a;b;bar');
e.detach(_bar);
(0, chai_1.expect)(e.hasListeners()).equals(true);
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('a;b;bar;a;b');
});
it('should remove from front', () => {
const _bar = e.add(bar);
e.add(a);
e.add(b);
(0, chai_1.expect)(e.hasListeners()).equals(true);
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('bar;a;b');
e.detach(_bar);
(0, chai_1.expect)(e.hasListeners()).equals(true);
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('bar;a;b;a;b');
});
it('should remove from middle', () => {
e.add(a);
const _bar = e.add(bar);
e.add(b);
(0, chai_1.expect)(e.hasListeners()).equals(true);
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('a;bar;b');
e.detach(_bar);
(0, chai_1.expect)(e.hasListeners()).equals(true);
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('a;bar;b;a;b');
});
it('emits to all event listeners after removing', () => {
e.add(a);
const _foo = e.add(foo);
e.add(b);
e.detach(_foo);
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('a;b');
});
it('can remove previous node in dispatch', () => {
const _foo = e.add(foo);
e.add(foo2);
e.add(a);
e.dispatch();
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('foo;foo2;a;foo2;a');
function foo2() {
pattern.push('foo2');
e.detach(_foo);
}
});
it('can remove next node in dispatch', () => {
e.add(a);
e.add(foo2);
const _foo = e.add(foo);
e.dispatch();
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('a;foo2;a;foo2'); // will remove node this dispatch (might be unexpected)
function foo2() {
pattern.push('foo2');
e.detach(_foo);
}
});
it('can remove node in dispatch', () => {
e.add(foo2);
e.add(a);
const _foo = e.add(foo);
e.dispatch();
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('foo2;a;foo2;a'); // will remove node this dispatch (might be unexpected)
function foo2() {
pattern.push('foo2');
e.detach(_foo);
}
});
it('can remove current node in dispatch', () => {
e.add(a);
const _foo = e.add(foo2);
e.add(b);
e.dispatch();
e.dispatch();
(0, chai_1.expect)(pattern.join(';')).equals('a;foo2;b;a;b');
function foo2() {
pattern.push('foo2');
e.detach(_foo);
}
});
it('can only detach from same signal', () => {
const e2 = new mini_signals_1.MiniSignal();
const binding = e.add(foo);
(0, chai_1.expect)(() => {
e2.detach(binding);
}).throws('MiniSignal#detach(): MiniSignal listener does not belong to this MiniSignal.');
(0, chai_1.expect)(e.hasListeners());
});
it('can be called multiple times', () => {
const binding = e.add(foo);
e.detach(binding);
e.detach(binding);
e.detach(binding);
});
});
describe('MiniSignal#detachAll', () => {
/* istanbul ignore next */
function oops() {
throw new Error('oops');
}
let e;
beforeEach(() => {
e = new mini_signals_1.MiniSignal();
});
it('removes all events', () => {
e.add(oops);
e.add(oops);
e.add(oops);
e.add(oops);
(0, chai_1.expect)(e.hasListeners()).equals(true);
(0, chai_1.expect)(e.detachAll()).equals(e);
(0, chai_1.expect)(e.hasListeners()).equals(false);
(0, chai_1.expect)(e.dispatch()).equals(false);
});
it('should not throw an error if no listeners are set', () => {
(0, chai_1.expect)(e.detachAll()).equals(e);
(0, chai_1.expect)(e.hasListeners()).equals(false);
(0, chai_1.expect)(e.dispatch()).equals(false);
});
it('Should not throw error when calling detach after detachAll', () => {
const binding = e.add(oops);
e.detachAll();
e.detach(binding);
});
});
describe('Garbage Collection', () => {
it('should clean up when signal is destroyed', () => __awaiter(void 0, void 0, void 0, function* () {
let e = new mini_signals_1.MiniSignal();
const eR = new WeakRef(e);
let fn = () => {
noop(e, w);
};
const fR = new WeakRef(fn);
const w = e.add(fn);
e.add(fn);
e.add(noop);
e.add(() => {
fn();
noop();
});
e.add(() => {
fn();
noop();
e.detach(w);
});
(0, chai_1.expect)(fR.deref()).to.exist;
e.dispatch();
// Removing references in this scope should mark nodes GC
fn = null;
e = null;
yield new Promise(resolve => setTimeout(resolve, 0));
global.gc();
(0, chai_1.expect)(fR.deref()).to.be.undefined;
(0, chai_1.expect)(eR.deref()).to.be.undefined;
// Only the node reference should be left
(0, chai_1.expect)(w).to.exist;
}));
it('should not leak memory after detach', () => __awaiter(void 0, void 0, void 0, function* () {
let e = new mini_signals_1.MiniSignal();
const eR = new WeakRef(e);
let fn = () => {
noop(e, fn);
};
const fR = new WeakRef(fn);
const w = e.add(fn);
fn = null;
(0, chai_1.expect)(fR.deref()).to.exist;
e.dispatch();
(0, chai_1.expect)(fR.deref()).to.exist;
e.detach(w);
yield new Promise(resolve => setTimeout(resolve, 0));
global.gc();
(0, chai_1.expect)(fR.deref()).to.be.undefined;
// should not throw an error when detaching gc ref
e.detach(w);
(0, chai_1.expect)(eR.deref()).to.exist;
// Also cleans up the signal
e = null;
yield new Promise(resolve => setTimeout(resolve, 0));
global.gc();
(0, chai_1.expect)(eR.deref()).to.be.undefined;
// Only the node reference should be left
(0, chai_1.expect)(w).to.exist;
}));
it('should not leak memory after detach all', () => __awaiter(void 0, void 0, void 0, function* () {
let e = new mini_signals_1.MiniSignal();
const eR = new WeakRef(e);
let fn = () => {
noop(e, fn);
};
const fR = new WeakRef(fn);
const w = e.add(fn);
fn = null;
(0, chai_1.expect)(fR.deref()).to.exist;
e.dispatch();
(0, chai_1.expect)(fR.deref()).to.exist;
e.detach(w);
yield new Promise(resolve => setTimeout(resolve, 0));
global.gc();
(0, chai_1.expect)(fR.deref()).to.be.undefined;
// should not throw an error when detaching gc ref
e.detachAll();
(0, chai_1.expect)(eR.deref()).to.exist;
// Also cleans up the signal
e = null;
yield new Promise(resolve => setTimeout(resolve, 0));
global.gc();
(0, chai_1.expect)(eR.deref()).to.be.undefined;
// Only the node reference should be left
(0, chai_1.expect)(w).to.exist;
}));
it('should clean up after itself when using add', () => __awaiter(void 0, void 0, void 0, function* () {
let e = new mini_signals_1.MiniSignal();
const eR = new WeakRef(e);
let w;
let fn = () => {
noop(e, w);
e.detach(w);
};
const fR = new WeakRef(fn);
w = e.add(fn);
fn = null;
(0, chai_1.expect)(fR.deref()).to.exist;
e.dispatch();
yield new Promise(resolve => setTimeout(resolve, 0));
global.gc();
(0, chai_1.expect)(fR.deref()).to.be.undefined;
// Also cleans up the signal
e = null;
yield new Promise(resolve => setTimeout(resolve, 0));
global.gc();
(0, chai_1.expect)(eR.deref()).to.be.undefined;
// Only the node reference should be left
(0, chai_1.expect)(w).to.exist;
}));
});
});
function noop(...args) {
// empty
}