sodiumjs
Version:
A Functional Reactive Programming (FRP) library for JavaScript
586 lines (507 loc) • 13.1 kB
text/typescript
import {
lambda1,
StreamSink,
StreamLoop,
CellSink,
Transaction,
Tuple2,
Operational,
Cell,
CellLoop,
getTotalRegistrations
} from '../../lib/Lib';
afterEach(() => {
if (getTotalRegistrations() != 0) {
throw new Error('listeners were not deregistered');
}
});
test('should test map()', (done) => {
const s = new StreamSink<number>();
const out: number[] = [];
const kill = s.map(a => a + 1)
.listen(a => {
out.push(a);
done();
});
s.send(7);
kill();
expect([8]).toEqual(out);
});
test('should throw an error send_with_no_listener_1', () => {
const s = new StreamSink<number>();
try {
s.send(7);
} catch (e) {
expect(e.message).toBe('send() was invoked before listeners were registered');
}
});
test('should (not?) throw an error send_with_no_listener_2', () => {
const s = new StreamSink<number>();
const out: number[] = [];
const kill = s.map(a => a + 1)
.listen(a => out.push(a));
s.send(7);
kill();
try {
// TODO: the message below is bit misleading, need to verify with Stephen B.
// - "this should not throw, because once() uses this mechanism"
s.send(9);
} catch (e) {
expect(e.message).toBe('send() was invoked before listeners were registered');
}
});
test('should map_tack', (done) => {
const s = new StreamSink<number>(),
t = new StreamSink<string>(),
out: number[] = [],
kill = s.map(lambda1((a: number) => a + 1, [t]))
.listen(a => {
out.push(a);
done();
});
s.send(7);
t.send("banana");
kill();
expect([8]).toEqual(out);
});
test('should test mapTo()', (done) => {
const s = new StreamSink<number>(),
out: string[] = [],
kill = s.mapTo("fusebox")
.listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
s.send(7);
s.send(9);
kill();
expect(['fusebox', 'fusebox']).toEqual(out);
});
test('should do mergeNonSimultaneous', (done) => {
const s1 = new StreamSink<number>(),
s2 = new StreamSink<number>(),
out: number[] = [];
const kill = s2.orElse(s1)
.listen(a => {
out.push(a);
if (out.length === 3) {
done();
}
});
s1.send(7);
s2.send(9);
s1.send(8);
kill();
expect([7, 9, 8]).toEqual(out);
});
test('should do mergeSimultaneous', (done) => {
const s1 = new StreamSink<number>((l: number, r: number) => { return r; }),
s2 = new StreamSink<number>((l: number, r: number) => { return r; }),
out: number[] = [],
kill = s2.orElse(s1)
.listen(a => {
out.push(a);
if (out.length === 5) {
done();
}
});
Transaction.run<void>(() => {
s1.send(7);
s2.send(60);
});
Transaction.run<void>(() => {
s1.send(9);
});
Transaction.run<void>(() => {
s1.send(7);
s1.send(60);
s2.send(8);
s2.send(90);
});
Transaction.run<void>(() => {
s2.send(8);
s2.send(90);
s1.send(7);
s1.send(60);
});
Transaction.run<void>(() => {
s2.send(8);
s1.send(7);
s2.send(90);
s1.send(60);
});
kill();
expect([60, 9, 90, 90, 90]).toEqual(out);
});
test('should do coalesce', (done) => {
const s = new StreamSink<number>((a, b) => a + b),
out: number[] = [],
kill = s.listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
Transaction.run<void>(() => {
s.send(2);
});
Transaction.run<void>(() => {
s.send(8);
s.send(40);
});
kill();
expect([2, 48]).toEqual(out);
});
test('should test filter()', (done) => {
const s = new StreamSink<number>(),
out: number[] = [],
kill = s.filter(a => a < 10)
.listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
s.send(2);
s.send(16);
s.send(9);
kill();
expect([2, 9]).toEqual(out);
});
test('should test filterNotNull()', (done) => {
const s = new StreamSink<string>(),
out: string[] = [],
kill = s.filterNotNull()
.listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
s.send("tomato");
s.send(null);
s.send("peach");
kill();
expect(["tomato", "peach"]).toEqual(out);
});
test('should test merge()', (done) => {
const sa = new StreamSink<number>(),
sb = sa.map(x => Math.floor(x / 10))
.filter(x => x != 0),
sc = sa.map(x => x % 10)
.merge(sb, (x, y) => x + y),
out: number[] = [],
kill = sc.listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
sa.send(2);
sa.send(52);
kill();
expect([2, 7]).toEqual(out);
});
test('should test loop()', (done) => {
const sa = new StreamSink<number>(),
sc = Transaction.run(() => {
const sb = new StreamLoop<number>(),
sc_ = sa.map(x => x % 10).merge(sb,
(x, y) => x + y),
sb_out = sa.map(x => Math.floor(x / 10))
.filter(x => x != 0);
sb.loop(sb_out);
return sc_;
}),
out: number[] = [],
kill = sc.listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
sa.send(2);
sa.send(52);
kill();
expect([2, 7]).toEqual(out);
});
test('should test gate()', (done) => {
const s = new StreamSink<string>(),
pred = new CellSink<boolean>(true),
out: string[] = [],
kill = s.gate(pred).listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
s.send("H");
pred.send(false);
s.send('O');
pred.send(true);
s.send('I');
kill();
expect(["H", "I"]).toEqual(out);
});
test('should test collect()', (done) => {
const ea = new StreamSink<number>(),
out: number[] = [],
sum = ea.collect(0, (a, s) => new Tuple2(a + s + 100, a + s)),
kill = sum.listen(a => {
out.push(a);
if (out.length === 5) {
done();
}
});
ea.send(5);
ea.send(7);
ea.send(1);
ea.send(2);
ea.send(3);
kill();
expect([105, 112, 113, 115, 118]).toEqual(out);
});
test('should test accum()', (done) => {
const ea = new StreamSink<number>(),
out: number[] = [],
sum = ea.accum(100, (a, s) => a + s),
kill = sum.listen(a => {
out.push(a);
if (out.length === 6) {
done();
}
});
ea.send(5);
ea.send(7);
ea.send(1);
ea.send(2);
ea.send(3);
kill();
expect([100, 105, 112, 113, 115, 118]).toEqual(out);
});
test('should test once()', (done) => {
const s = new StreamSink<string>(),
out: string[] = [],
kill = s.once().listen(a => {
out.push(a);
done();
});
s.send("A");
s.send("B");
s.send("C");
kill();
expect(["A"]).toEqual(out);
});
test('should test defer()', (done) => {
const s = new StreamSink<string>(),
c = s.hold(" "),
out: string[] = [],
kill = Operational.defer(s).snapshot1(c)
.listen(a => {
out.push(a);
if (out.length === 3) {
done();
}
});
s.send("C");
s.send("B");
s.send("A");
kill();
expect(["C", "B", "A"]).toEqual(out);
});
test('should test hold()', (done) => {
const s = new StreamSink<number>(),
c = s.hold(0),
out: number[] = [],
kill = Operational.updates(c)
.listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
s.send(2);
s.send(9);
kill();
expect([2, 9]).toEqual(out);
});
test('should do holdIsDelayed', (done) => {
const s = new StreamSink<number>(),
h = s.hold(0),
sPair = s.snapshot(h, (a, b) => a + " " + b),
out: string[] = [],
kill = sPair.listen(a => {
out.push(a);
if (out.length === 2) {
done();
}
});
s.send(2);
s.send(3);
kill();
expect(["2 0", "3 2"]).toEqual(out);
});
test('should test switchC()', (done) => {
class SC {
constructor(a: string, b: string, sw: string) {
this.a = a;
this.b = b;
this.sw = sw;
}
a: string;
b: string;
sw: string;
}
const ssc = new StreamSink<SC>(),
// Split each field out of SC so we can update multiple cells in a
// single transaction.
ca = ssc.map(s => s.a).filterNotNull().hold("A"),
cb = ssc.map(s => s.b).filterNotNull().hold("a"),
csw_str = ssc.map(s => s.sw).filterNotNull().hold("ca"),
// ****
// NOTE! Because this lambda contains references to Sodium objects, we
// must declare them explicitly using lambda1() so that Sodium knows
// about the dependency, otherwise it can't manage the memory.
// ****
csw = csw_str.map(lambda1(s => s == "ca" ? ca : cb, [ca, cb])),
co = Cell.switchC(csw),
out: string[] = [],
kill = co.listen(c => {
out.push(c);
if (out.length === 11) {
done();
}
});
ssc.send(new SC("B", "b", null));
ssc.send(new SC("C", "c", "cb"));
ssc.send(new SC("D", "d", null));
ssc.send(new SC("E", "e", "ca"));
ssc.send(new SC("F", "f", null));
ssc.send(new SC(null, null, "cb"));
ssc.send(new SC(null, null, "ca"));
ssc.send(new SC("G", "g", "cb"));
ssc.send(new SC("H", "h", "ca"));
ssc.send(new SC("I", "i", "ca"));
kill();
expect(["A", "B", "c", "d", "E", "F", "f", "F", "g", "H", "I"]).toEqual(out);
});
test('should test switchS()', (done) => {
class SS {
constructor(a: string, b: string, sw: string) {
this.a = a;
this.b = b;
this.sw = sw;
}
a: string;
b: string;
sw: string;
}
const sss = new StreamSink<SS>(),
sa = sss.map(s => s.a),
sb = sss.map(s => s.b),
csw_str = sss.map(s => s.sw).filterNotNull().hold("sa"),
// ****
// NOTE! Because this lambda contains references to Sodium objects, we
// must declare them explicitly using lambda1() so that Sodium knows
// about the dependency, otherwise it can't manage the memory.
// ****
csw = csw_str.map(lambda1(sw => sw == "sa" ? sa : sb, [sa, sb])),
so = Cell.switchS(csw),
out: string[] = [],
kill = so.listen(x => {
out.push(x);
if (out.length === 9) {
done();
}
});
sss.send(new SS("A", "a", null));
sss.send(new SS("B", "b", null));
sss.send(new SS("C", "c", "sb"));
sss.send(new SS("D", "d", null));
sss.send(new SS("E", "e", "sa"));
sss.send(new SS("F", "f", null));
sss.send(new SS("G", "g", "sb"));
sss.send(new SS("H", "h", "sa"));
sss.send(new SS("I", "i", "sa"));
kill();
expect(["A", "B", "C", "d", "e", "F", "G", "h", "I"]).toEqual(out);
});
test('should do switchSSimultaneous', (done) => {
class SS2 {
s: StreamSink<number> = new StreamSink<number>();
}
const ss1 = new SS2(),
ss2 = new SS2(),
ss3 = new SS2(),
ss4 = new SS2(),
css = new CellSink<SS2>(ss1),
// ****
// NOTE! Because this lambda contains references to Sodium objects, we
// must declare them explicitly using lambda1() so that Sodium knows
// about the dependency, otherwise it can't manage the memory.
// ****
so = Cell.switchS(css.map(lambda1((b: SS2) => b.s, [ss1.s, ss2.s, ss3.s, ss4.s]))),
out: number[] = [],
kill = so.listen(c => {
out.push(c);
if (out.length === 10) {
done();
}
});
ss1.s.send(0);
ss1.s.send(1);
ss1.s.send(2);
css.send(ss2);
ss1.s.send(7);
ss2.s.send(3);
ss2.s.send(4);
ss3.s.send(2);
css.send(ss3);
ss3.s.send(5);
ss3.s.send(6);
ss3.s.send(7);
Transaction.run(() => {
ss3.s.send(8);
css.send(ss4);
ss4.s.send(2);
});
ss4.s.send(9);
kill();
expect([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]).toEqual(out);
});
test('should test loopCell', (done) => {
const sa = new StreamSink<number>(),
sum_out = Transaction.run(() => {
const sum = new CellLoop<number>(),
sum_out_ = sa.snapshot(sum, (x, y) => x + y).hold(0);
sum.loop(sum_out_);
return sum_out_;
}),
out: number[] = [],
kill = sum_out.listen(a => {
out.push(a);
if (out.length === 4) {
done();
}
});
sa.send(2);
sa.send(3);
sa.send(1);
kill();
expect([0, 2, 5, 6]).toEqual(out);
expect(6).toBe(sum_out.sample());
});
test('should test defer/split memory cycle', done => {
// We do not fire through sl here, as it would cause an infinite loop.
// This is just a memory management test.
let sl : StreamLoop<number>;
Transaction.run(() => {
sl = new StreamLoop<number>();
sl.loop(Operational.defer(sl));
});
let kill = sl.listen(() => {});
kill();
done();
});