xstream
Version:
An extremely intuitive, small, and fast functional reactive stream library for JavaScript
336 lines (294 loc) • 10.4 kB
text/typescript
/// <reference path="../../typings/globals/mocha/index.d.ts" />
/// <reference path="../../typings/globals/node/index.d.ts" />
import xs, {Stream, Listener} from '../../src/index';
import fromDiagram from '../../src/extra/fromDiagram';
import * as assert from 'assert';
describe('Stream.prototype.flatten', () => {
describe('with map+debug to break the fusion', () => {
it('should not restart inner stream if switching to the same inner stream', (done) => {
const outer = fromDiagram('-A---------B----------C--------|');
const nums = fromDiagram( '-a-b-c-----------------------|', {
values: {a: 1, b: 2, c: 3}
});
const inner = nums.map(x => 10 * x);
const stream = outer.map(() => inner).debug(() => { }).flatten();
const expected = [10, 20, 30];
stream.addListener({
next: (x: number) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
done();
}
});
});
});
describe('with map', () => {
it('should expand each periodic event with 3 sync events', (done) => {
const source: Stream<Stream<number>> = xs.periodic(100).take(3)
.map((i: number) => xs.of(1 + i, 2 + i, 3 + i));
const stream: Stream<number> = source.flatten();
const expected = [1, 2, 3, 2, 3, 4, 3, 4, 5];
stream.addListener({
next: (x: number) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
done();
}
});
});
it('should have an ins field as metadata', (done) => {
const source: Stream<number> = xs.periodic(100).take(3);
const stream: Stream<number> = source
.map((i: number) => xs.of(1 + i, 2 + i, 3 + i))
.flatten();
assert.strictEqual(stream['_prod']['ins'], source);
done();
});
it('should return a flat stream with correct TypeScript types', (done) => {
const streamStrings: Stream<string> = Stream.create({
start: (listener: Listener<string>) => {},
stop: () => {}
});
const streamBooleans: Stream<boolean> = Stream.create({
start: (listener: Listener<boolean>) => {},
stop: () => {}
});
// Type checked by the compiler. Without Stream<boolean> it does not compile.
const flat: Stream<boolean> = streamStrings.map(x => streamBooleans).flatten();
done();
});
it('should expand 3 sync events as a periodic, only last one passes', (done) => {
const stream = xs.fromArray([1, 2, 3])
.map(i => xs.periodic(100 * i).take(2).map(x => `${i}${x}`))
.flatten();
// ---x---x---x---x---x---x
// ---10--11
// -------20------21
// -----------30----------31
const expected = ['30', '31'];
stream.addListener({
next: (x: string) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
done();
}
});
});
it('should expand 3 async events as a periodic each', (done) => {
const stream = xs.periodic(140).take(3)
.map(i =>
xs.periodic(100 * (i < 2 ? 1 : i)).take(3).map(x => `${i}${x}`)
)
.flatten();
// ---x---x---x---x---x---x---x---x---x---x---x---x
// ---00--01--02
// ----10--11--12
// ------------20-----------21----------22
const expected = ['00', '10', '20', '21', '22'];
stream.addListener({
next: (x: string) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
done();
}
});
});
it('should expand 3 async events as a periodic each, no optimization', (done) => {
const stream = xs.periodic(140).take(3)
.map(i =>
xs.periodic(100 * (i < 2 ? 1 : i)).take(3).map(x => `${i}${x}`)
)
.filter(() => true) // breaks the optimization map+flattenConcurrently
.flatten();
// ---x---x---x---x---x---x---x---x---x---x---x---x
// ---00--01--02
// ----10--11--12
// ------------20-----------21----------22
const expected = ['00', '10', '20', '21', '22'];
stream.addListener({
next: (x: string) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
done();
}
});
});
it('should propagate user mistakes in project as errors', (done) => {
const source = xs.periodic(30).take(1);
const stream = source.map(
x => {
const y = (<string> <any> x).toLowerCase();
return xs.of(y);
}
).flatten();
stream.addListener({
next: () => done('next should not be called'),
error: (err) => {
assert.notStrictEqual(err.message.match(/is not a function$/), null);
done();
},
complete: () => {
done('complete should not be called');
},
});
});
it('should not leak when used in a withLatestFrom-like case', (done) => {
const a$ = xs.periodic(100);
const b$ = xs.periodic(220);
let innerAEmissions = 0;
// a$.withLatestFrom(b$, (a, b) => a + b)
const c$ = b$.map(b =>
a$.map(a => a + b).debug(a => { innerAEmissions += 1; })
).flatten().take(1);
let cEmissions = 0;
c$.addListener({
next: (c) => {
assert.strictEqual(cEmissions, 0);
assert.strictEqual(c, 0);
cEmissions += 1;
},
error: (err: any) => done(err),
complete: () => { },
});
setTimeout(() => {
assert.strictEqual(innerAEmissions, 1);
assert.strictEqual(cEmissions, 1);
done();
}, 800);
});
it('should not error when stopping, and outer stream was empty', (done) => {
const outer = xs.never();
const stream = outer.map(x => xs.of(1, 2, 3)).flatten();
const listener = {
next: () => {},
error: () => {},
complete: () => {},
};
assert.doesNotThrow(() => {
stream.addListener(listener);
stream.removeListener(listener);
});
setTimeout(() => done(), 500);
});
it('should allow switching inners asynchronously without restarting source', (done) => {
const outer = fromDiagram( '-A---------B----------C------|');
const periodic = fromDiagram('---a-b--c----d--e--f----g--h-|', {
values: { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8 }
});
const stream = outer.map(x => {
if (x === 'A') {
return periodic.map(i => i * 10);
} else if (x === 'B') {
return periodic.map(i => i * 100);
} else if (x === 'C') {
return periodic.map(i => i * 1000);
}
}).flatten();
const expected = [10, 20, 30, 400, 500, 600, 7000, 8000];
stream.addListener({
next: (x: number) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
done();
}
});
});
it('should not restart inner stream if switching to the same inner stream', (done) => {
const outer = fromDiagram('-A---------B----------C--------|');
const nums = fromDiagram( '-a-b-c-----------------------|', {
values: {a: 1, b: 2, c: 3}
});
const inner = nums.map(x => 10 * x);
const stream = outer.map(() => inner).flatten();
const expected = [10, 20, 30];
stream.addListener({
next: (x: number) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
done();
}
});
});
});
describe('with filter+map fusion', () => {
it('should execute the predicate, the projection, and the flattening', (done) => {
let predicateCallCount = 0;
let projectCallCount = 0;
const stream = xs.periodic(140).take(3)
.filter(i => {
predicateCallCount += 1;
return i % 2 === 0;
})
.map(i => {
projectCallCount += 1;
return xs.periodic(100 * (i < 2 ? 1 : i)).take(3).map(x => `${i}${x}`);
})
.flatten();
// ---x---x---x---x---x---x---x---x---x---x---x---x
// ---00--01--02
// ------------20-----------21----------22
const expected = ['00', '01', '20', '21', '22'];
stream.addListener({
next: (x: string) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
assert.equal(predicateCallCount, 3);
assert.equal(projectCallCount, 2);
done();
}
});
});
});
describe('with mapTo', () => {
it('should have the correct \'type\' metadata on the operator producer', (done) => {
const source: Stream<Stream<number>> = xs.periodic(100).take(3)
.mapTo(xs.of(1, 2, 3));
const stream: Stream<number> = source.flatten();
assert.strictEqual(stream['_prod']['type'], 'mapTo+flatten');
done();
});
it('should not restart inner stream if switching to the same inner stream', (done) => {
const outer = fromDiagram('-A---------B----------C--------|');
const nums = fromDiagram( '-a-b-c-----------------------|', {
values: {a: 1, b: 2, c: 3}
});
const inner = nums.map(x => 10 * x);
const stream = outer.mapTo(inner).flatten();
const expected = [10, 20, 30];
stream.addListener({
next: (x: number) => {
assert.equal(x, expected.shift());
},
error: (err: any) => done(err),
complete: () => {
assert.equal(expected.length, 0);
done();
}
});
});
});
});