Repository: mostjs/core (ts)

Issue:
Return canonical empty in merge, mergeArray
See #152

Pull request:
Filter out canonical empty from merge, mergeArray
Close #195

Patch:
diff --git a/packages/core/src/combinator/combine.js b/packages/core/src/combinator/combine.js
index aee954a..63254f6 100644
--- a/packages/core/src/combinator/combine.js
+++ b/packages/core/src/combinator/combine.js
@@ -1,9 +1,7 @@
-/** @license MIT License (c) copyright 2010-2016 original author or authors */
-/** @author Brian Cavalier */
-/** @author John Hann */
+/** @license MIT License (c) copyright 2010 original author or authors */
 
 import { map } from './transform'
-import { empty } from '../source/empty'
+import { empty, containsCanonicalEmpty } from '../source/empty'
 import Pipe from '../sink/Pipe'
 import IndexSink from '../sink/IndexSink'
 import { disposeAll, tryDispose } from '@most/disposable'
@@ -15,9 +13,8 @@ import invoke from '../invoke'
  * @returns {Stream} stream containing the result of applying f to the most recent
  *  event of each input stream, whenever a new event arrives on any stream.
  */
-export function combine (f, stream1, stream2) {
-  return combineArray(f, [stream1, stream2])
-}
+export const combine = (f, stream1, stream2) =>
+  combineArray(f, [stream1, stream2])
 
 /**
 * Combine latest events from all input streams
@@ -27,7 +24,7 @@ export function combine (f, stream1, stream2) {
 *  event of each input stream, whenever a new event arrives on any stream.
 */
 export const combineArray = (f, streams) =>
-  streams.length === 0 ? empty()
+  streams.length === 0 || containsCanonicalEmpty(streams) ? empty()
     : streams.length === 1 ? map(f, streams[0])
     : new Combine(f, streams)
 
diff --git a/packages/core/src/combinator/merge.js b/packages/core/src/combinator/merge.js
index 5dd46d1..f6b4c64 100644
--- a/packages/core/src/combinator/merge.js
+++ b/packages/core/src/combinator/merge.js
@@ -4,7 +4,7 @@
 
 import Pipe from '../sink/Pipe'
 import IndexSink from '../sink/IndexSink'
-import { empty } from '../source/empty'
+import { empty, isCanonicalEmpty } from '../source/empty'
 import { disposeAll, tryDispose } from '@most/disposable'
 import { reduce } from '@most/prelude'
 
@@ -23,9 +23,7 @@ export function merge (stream1, stream2) {
  * arbitrary order.
  */
 export const mergeArray = streams =>
-  streams.length === 0 ? empty()
-    : streams.length === 1 ? streams[0]
-    : mergeStreams(streams)
+  mergeStreams(withoutCanonicalEmpty(streams))
 
 /**
  * This implements fusion/flattening for merge.  It will
@@ -37,7 +35,15 @@ export const mergeArray = streams =>
  * merge operations into a single merge.
  */
 const mergeStreams = streams =>
-  new Merge(reduce(appendSources, [], streams))
+  streams.length === 0 ? empty()
+    : streams.length === 1 ? streams[0]
+    : new Merge(reduce(appendSources, [], streams))
+
+const withoutCanonicalEmpty = streams =>
+  streams.filter(isNotCanonicalEmpty)
+
+const isNotCanonicalEmpty = stream =>
+  !isCanonicalEmpty(stream)
 
 const appendSources = (sources, stream) =>
   sources.concat(stream instanceof Merge ? stream.sources : stream)


Test patch:
diff --git a/packages/core/test/combinator/merge-test.js b/packages/core/test/combinator/merge-test.js
index 8faa0ed..f7452c2 100644
--- a/packages/core/test/combinator/merge-test.js
+++ b/packages/core/test/combinator/merge-test.js
@@ -1,13 +1,33 @@
 import { describe, it } from 'mocha'
-import { eq } from '@briancavalier/assert'
+import { assert, is, eq } from '@briancavalier/assert'
 
 import { now } from '../../src/source/now'
+import { empty, isCanonicalEmpty } from '../../src/source/empty'
 import { delay } from '../../src/combinator/delay'
 import { merge, mergeArray } from '../../src/combinator/merge'
 
 import { makeEventsFromArray, collectEventsFor } from '../helper/testEnv'
 
 describe('merge', function () {
+  it('given one canonical empty, should return other stream', () => {
+    const s = now(0)
+    is(s, merge(s, empty()))
+  })
+
+  it('given one canonical empty, should return other stream', () => {
+    const s = now(1)
+    is(s, merge(empty(), s))
+  })
+
+  it('given both canonical empty, should return canonical empty', () => {
+    assert(isCanonicalEmpty(merge(empty(), empty())))
+  })
+
+  it('should recursively return canonical empty', () => {
+    const s = merge(empty(), merge(empty(), empty()))
+    assert(isCanonicalEmpty(s))
+  })
+
   it('should include items from all inputs', function () {
     return testMerge(merge)
   })
@@ -27,6 +47,24 @@ describe('merge', function () {
 })
 
 describe('mergeArray', function () {
+  it('given empty array, should return canonical empty', () => {
+    assert(isCanonicalEmpty(mergeArray([])))
+  })
+
+  it('given array containing only canonical empty, should return canonical empty', () => {
+    assert(isCanonicalEmpty(mergeArray([empty(), empty(), empty()])))
+  })
+
+  it('should recursively return canonical empty', () => {
+    const s = mergeArray([empty(), mergeArray([empty(), empty()])])
+    assert(isCanonicalEmpty(s))
+  })
+
+  it('given array containing 1 non-canonical empty stream, should return that stream', () => {
+    const s = now(1)
+    is(s, mergeArray([empty(), s, empty()]))
+  })
+
   it('should include items from all inputs', function () {
     return testMerge((s1, s2) => mergeArray([s1, s2]))
   })
diff --git a/packages/core/test/combine-test.js b/packages/core/test/combine-test.js
index 3868f54..72b1d59 100644
--- a/packages/core/test/combine-test.js
+++ b/packages/core/test/combine-test.js
@@ -1,15 +1,31 @@
 import { describe, it } from 'mocha'
-import { eq } from '@briancavalier/assert'
+import { assert, eq } from '@briancavalier/assert'
 
 import { combine, combineArray } from '../src/combinator/combine'
 import { delay } from '../src/combinator/delay'
 import { now } from '../src/source/now'
+import { empty, isCanonicalEmpty } from '../src/source/empty'
 
 import { collectEventsFor, makeEvents, makeEventsFromArray } from './helper/testEnv'
 
 const sentinel = { value: 'sentinel' }
 
 describe('combine', function () {
+  it('given one canonical empty stream, should return canonical empty', () => {
+    const s = combine(Array, now(0), empty())
+    assert(isCanonicalEmpty(s))
+  })
+
+  it('given one canonical empty stream, should return canonical empty', () => {
+    const s = combine(Array, empty(), now(1))
+    assert(isCanonicalEmpty(s))
+  })
+
+  it('given two canonical empty streams, should return canonical empty', () => {
+    const s = combine(Array, empty(), empty())
+    assert(isCanonicalEmpty(s))
+  })
+
   it('should yield initial only after all inputs yield', function () {
     const s1 = now(1)
     const s2 = now(sentinel)
@@ -40,6 +56,16 @@ describe('combine', function () {
 })
 
 describe('combineArray', function () {
+  it('given empty array, should return canonical empty', () => {
+    const s = combineArray(Array, [])
+    assert(isCanonicalEmpty(s))
+  })
+
+  it('given array containing canonical empty, should return canonical empty', () => {
+    const s = combineArray(Array, [now(0), empty(), now(1)])
+    assert(isCanonicalEmpty(s))
+  })
+
   it('should yield initial only after all inputs yield', function () {
     const s1 = now(1)
     const s2 = now(sentinel)