UNPKG

mini-signals

Version:
541 lines (540 loc) 21 kB
"use strict"; 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 }