callbag-merge
Version:
Callbag factory that merges data from multiple callbag sources
556 lines (489 loc) • 13.6 kB
JavaScript
const test = require('tape');
const merge = require('.');
test('it merges one async finite listenable source', t => {
t.plan(14);
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
[1, 'number'],
[2, 'undefined'],
];
const downwardsExpected = [1, 2, 3];
function sourceA(type, data) {
if (type === 0) {
const sink = data;
let i = 0;
const id = setInterval(() => {
i++;
sink(1, i);
if (i === 3) {
clearInterval(id);
sink(2);
}
}, 100);
sink(0, sourceA);
}
}
function sink(type, data) {
const et = downwardsExpectedType.shift();
t.equals(type, et[0], 'downwards type is expected: ' + et[0]);
t.equals(typeof data, et[1], 'downwards data type is expected: ' + et[1]);
if (type === 1) {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
}
}
const source = merge(sourceA);
source(0, sink);
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 700);
});
test('it merges two async finite listenable sources', t => {
t.plan(17);
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
[1, 'string'],
[1, 'number'],
[2, 'undefined'],
];
const downwardsExpected = [1, 2, 'a', 3];
function sourceA(type, data) {
if (type === 0) {
const sink = data;
let i = 0;
const id = setInterval(() => {
i++;
sink(1, i);
if (i === 3) {
clearInterval(id);
sink(2);
}
}, 100);
sink(0, sourceA);
}
}
function sourceB(type, data) {
if (type === 0) {
const sink = data;
setTimeout(() => {
sink(1, 'a');
setTimeout(() => {
sink(2);
}, 250);
}, 250);
sink(0, sourceB);
}
}
function sink(type, data) {
const et = downwardsExpectedType.shift();
t.equals(type, et[0], 'downwards type is expected: ' + et[0]);
t.equals(typeof data, et[1], 'downwards data type is expected: ' + et[1]);
if (type === 1) {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
}
}
const source = merge(sourceA, sourceB);
source(0, sink);
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 700);
});
test('it returns a source that disposes upon upwards END', t => {
t.plan(16);
const upwardsExpected = [[0, 'function'], [2, 'undefined']];
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
[1, 'number'],
];
const downwardsExpected = [10, 20, 30];
function makeSource() {
let sent = 0;
let id;
const source = (type, data) => {
const e = upwardsExpected.shift();
t.equals(type, e[0], 'upwards type is expected: ' + e[0]);
t.equals(typeof data, e[1], 'upwards data is expected: ' + e[1]);
if (type === 0) {
const sink = data;
id = setInterval(() => {
sink(1, ++sent * 10);
}, 100);
sink(0, source);
} else if (type === 2) {
clearInterval(id);
}
};
return source;
}
function makeSink(type, data) {
let talkback;
return (type, data) => {
const et = downwardsExpectedType.shift();
t.equals(type, et[0], 'downwards type is expected: ' + et[0]);
t.equals(typeof data, et[1], 'downwards data type is expected: ' + et[1]);
if (type === 0) {
talkback = data;
}
if (type === 1) {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
}
if (downwardsExpected.length === 0) {
talkback(2);
}
};
}
const source = merge(makeSource());
const sink = makeSink();
source(0, sink);
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 700);
});
test('it errors when one of the sources errors', t => {
t.plan(21);
const upwardsExpectedA = [[0, 'function']];
const upwardsExpectedB = [[0, 'function'], [2, 'undefined']];
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
[1, 'number'],
[2, 'string'],
];
const downwardsExpected = [11, 101, 12, 'err'];
function makeSourceA() {
let id;
let count = 0;
const sourceA = (type, data) => {
const e = upwardsExpectedA.shift();
t.equals(type, e[0], 'upwards type is expected: ' + e[0]);
t.equals(typeof data, e[1], 'upwards data is expected: ' + e[1]);
if (type === 0) {
const sink = data;
id = setInterval(() => {
sink(1, ++count + 10);
if (count < 2) {
return;
}
clearInterval(id);
sink(2, 'err');
}, 20);
sink(0, sourceA);
} else if (type === 2) {
t.fail('Errored source should not receive unsubscribing 2 from merge.');
}
};
return sourceA;
}
function makeSourceB() {
let id;
let count = 0
const sourceB = (type, data) => {
const e = upwardsExpectedB.shift();
t.equals(type, e[0], 'upwards type is expected: ' + e[0]);
t.equals(typeof data, e[1], 'upwards data is expected: ' + e[1]);
if (type === 0) {
const sink = data;
id = setInterval(() => {
sink(1, ++count + 100);
}, 30);
sink(0, sourceB);
} else if (type === 2) {
clearInterval(id);
}
};
return sourceB;
}
function makeSink(type, data) {
return (type, data) => {
const et = downwardsExpectedType.shift();
t.equals(type, et[0], 'downwards type is expected: ' + et[0]);
t.equals(typeof data, et[1], 'downwards data type is expected: ' + et[1]);
if (type === 0) {
talkback = data;
} else {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
}
};
}
const source = merge(makeSourceA(), makeSourceB());
const sink = makeSink();
source(0, sink);
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 700);
});
test('it greets the sink as soon as the first member-source greets', t => {
t.plan(11);
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
[1, 'string'],
[2, 'undefined'],
];
const downwardsExpected = [10, 20, 'a'];
let sinkGreeted = false;
function quickSource(start, sink) {
if (start !== 0) return;
t.equals(sinkGreeted, false, 'sink not yet greeted before any member-source greets');
sink(0, quickSource);
t.equals(sinkGreeted, true, 'sink greeted right after quick member-source greets');
sink(1, 10);
sink(1, 20);
sink(2);
}
function slowSource(start, sink) {
if (start !== 0) return;
setTimeout(() => {
sink(0, slowSource);
sink(1, 'a');
sink(2);
}, 50);
}
function sink(type, data) {
const et = downwardsExpectedType.shift();
t.deepEquals([type, typeof data], et, 'downwards type is expected: ' + et);
if (type === 0) {
sinkGreeted = true;
}
if (type === 1) {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
}
}
const source = merge(quickSource, slowSource);
source(0, sink);
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 500);
});
test('it merges sync listenable sources, resilient to greet/terminate race conditions, part 1', t => {
t.plan(9);
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
[1, 'string'],
[2, 'undefined'],
];
const downwardsExpected = [10, 20, 'a'];
function sourceA(start, sink) {
if (start !== 0) return;
sink(0, sourceA);
sink(1, 10);
sink(1, 20);
sink(2);
}
function sourceB(start, sink) {
if (start !== 0) return;
sink(0, sourceB);
setTimeout(() => {
sink(1, 'a');
sink(2);
}, 50);
}
function sink(type, data) {
const et = downwardsExpectedType.shift();
t.deepEquals([type, typeof data], et, 'downwards type is expected: ' + et);
if (type === 1) {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
}
}
const source = merge(sourceA, sourceB);
source(0, sink);
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 500);
});
test('it merges sync listenable sources, resilient to greet/terminate race conditions, part 2', t => {
t.plan(9);
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
[1, 'string'],
[2, 'undefined'],
];
const downwardsExpected = [10, 20, 'a'];
function sourceA(start, sink) {
if (start !== 0) return;
sink(0, sourceA);
sink(1, 10);
sink(1, 20);
sink(2);
}
function sourceB(start, sink) {
if (start !== 0) return;
sink(0, sourceB);
setTimeout(() => {
sink(1, 'a');
sink(2);
}, 50);
}
function sink(type, data) {
const et = downwardsExpectedType.shift();
t.deepEquals([type, typeof data], et, 'downwards type is expected: ' + et);
if (type === 1) {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
}
}
const source = merge(sourceB, sourceA);
source(0, sink);
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 500);
});
test('it merges sync listenable sources, resilient to greet/error race conditions, part 3', t => {
t.plan(7);
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
[2, 'string'],
];
const downwardsExpected = [10, 20, 'err'];
function sourceA(start, sink) {
if (start !== 0) return;
sink(0, sourceA);
sink(1, 10);
sink(1, 20);
sink(2, 'err');
}
function sourceB(start, sink) {
if (start !== 0) return;
t.fail('sourceB should not get subscribed.');
sink(0, sourceB);
sink(1, 'a');
sink(2);
}
function sink(type, data) {
const et = downwardsExpectedType.shift();
t.deepEquals([type, typeof data], et, 'downwards type is expected: ' + et);
if (type === 1) {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
}
}
const source = merge(sourceA, sourceB);
source(0, sink);
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 500);
});
test('it merges sync listenable sources, resilient to greet/disposal race conditions', t => {
t.plan(6);
const downwardsExpectedType = [
[0, 'function'],
[1, 'number'],
[1, 'number'],
];
const downwardsExpected = [10, 20];
function sourceA(start, sink) {
if (start !== 0) return;
sink(0, sourceA);
sink(1, 10);
sink(1, 20);
}
function sourceB(start, sink) {
if (start !== 0) return;
t.fail('sourceB should not get subscribed.');
sink(0, sourceB);
sink(1, 'a');
}
const makeSink = () => {
let limit = 2;
let talkback;
return (type, data) => {
const et = downwardsExpectedType.shift();
t.deepEquals([type, typeof data], et, 'downwards type is expected: ' + et);
if (type === 0) {
talkback = data;
}
if (type === 1) {
const e = downwardsExpected.shift();
t.equals(data, e, 'downwards data is expected: ' + e);
if (--limit === 0) {
talkback(2);
}
}
}
};
const source = merge(sourceA, sourceB);
source(0, makeSink());
setTimeout(() => {
t.pass('nothing else happens');
t.end();
}, 500);
});
test('all sources get requests from sinks', t => {
let history = [];
const report = (name,dir,t,d) => t !== 0 && history.push([name,dir,t,d]);
const source1 = makeMockCallbag('source1', report, true);
const source2 = makeMockCallbag('source2', report, true);
const source3 = makeMockCallbag('source3', report, true);
const sink = makeMockCallbag('sink');
merge(source1, source2, source3)(0, sink);
sink.emit(1);
sink.emit(2);
t.deepEqual(history, [
['source1', 'fromDown', 1, undefined],
['source2', 'fromDown', 1, undefined],
['source3', 'fromDown', 1, undefined],
['source1', 'fromDown', 2, undefined],
['source2', 'fromDown', 2, undefined],
['source3', 'fromDown', 2, undefined],
], 'sources all get requests from sink');
t.end();
});
test('all sources get subscription errors from sink', t => {
let history = [];
const report = (name,dir,t,d) => t !== 0 && history.push([name,dir,t,d]);
const source1 = makeMockCallbag('source1', report, true);
const source2 = makeMockCallbag('source2', report, true);
const source3 = makeMockCallbag('source3', report, true);
const sink = makeMockCallbag('sink');
merge(source1, source2, source3)(0, sink);
sink.emit(2, 'err');
t.deepEqual(history, [
['source1', 'fromDown', 2, 'err'],
['source2', 'fromDown', 2, 'err'],
['source3', 'fromDown', 2, 'err'],
], 'all sources get errors from sink');
t.end();
});
function makeMockCallbag(name, report=()=>{}, isSource) {
if (report === true) {
isSource = true;
report = ()=>{};
}
let talkback;
let mock = (t, d) => {
report(name, 'fromUp', t, d);
if (t === 0){
talkback = d;
if (isSource) talkback(0, (st, sd) => report(name, 'fromDown', st, sd));
}
};
mock.emit = (t, d) => talkback(t, d);
return mock;
}