Looks like a job for the scan operator
// substitute appropriate real-world logic
const isProperlyFormed = (x) => x === 'ab'  
const isIncomplete = (x) => x[0] === 'a' && x.length === 1
const startsWithEnding = (x) => x[0] === 'b'
const getCorrected = (buffer, x) => buffer.prev + x[0]
const getTail = (buffer, x) => x.slice(1)
const initialBuffer = {
  emit: [],
  prev: null
}
const result = source
  .scan((buffer, x) => {
    if (isProperlyFormed(x)) {
      buffer = {emit: [x], prev:null}
    }
    if (isIncomplete(x)) {
      buffer = {emit: [], prev:x}
    }
    if (startsWithEnding(x)) {
      const corrected = getCorrected(buffer, x)
      const tail = getTail(buffer, x)
      if (isProperlyFormed(tail)) {
        buffer = {emit: [corrected, tail], prev: null}
      } else {
        buffer = {emit: [corrected], prev: tail}
      }
    }
    return buffer
  }, initialBuffer)
  .flatMap(x => x.emit)
Working CodePen 
Edit
Looking at the test input stream, I think a case is missing, which will break the above.  
I changed the test from
---ab---ab---a---ba---bab---ab---ab---ab--->
to  
---ab---ab---a---ba---bab---aba---b---ab--->
and also slimmed down the algorithm
const getNextBuffer = (x) => {
  const items = x.split(/(ab)/g).filter(y => y)  // get valid items plus tail
  return {
    emit: items.filter(x => x === 'ab'),    // emit valid items
    save: items.filter(x => x !== 'ab')[0]  // save tail
  }
}
const initialBuffer = {
  emit: [],
  save: null
}
const result = source
  .scan((buffer, item) => {
    const bufferAndItem = (buffer.save ? buffer.save : '') + item
    return getNextBuffer(bufferAndItem)
  }, initialBuffer)
  .flatMap(x => x.emit)
Working example CodePen