Skip to content

Instantly share code, notes, and snippets.

@rpominov
Last active May 4, 2016 22:34
Show Gist options
  • Save rpominov/acb11abfb5383be6db70 to your computer and use it in GitHub Desktop.
Save rpominov/acb11abfb5383be6db70 to your computer and use it in GitHub Desktop.
Router concept
// trnsitionRequests -> BrowserAPI -> [ resolveRoute -> loadData -> render -> restoreScroll ]

// poor man's strem.flatMapLatest()
router.pipe((payload, next) => {
  next(1)
  next(2)
  return () => {
    // cleanup
  }
})

createRouter(callback => {
  browserHistoryAPI.listen(callback)
})
.pipe(createNestedReactResolver(...))
.pipe(createReactDataLoader(...))
.pipe(createNestedReactRenderer(...))
.pipe(createScrollRestorer(...))
@rpominov
Copy link
Author

Open questions:

Redirects

In browser: don't call next(), call transitionRequest()
on server: we need to bypass other steps of pipeline somehow, redirect is the final result (but do we even use pipeline on the server?)

Errors handling

Correctly handled (expected) error: pass down the pipeline a result that next handlers can correctly handle. For example in data loading, we can pass down either data or error; then the next handler, react renderer, can render normal pages or the error page; and the next one, scroll behavior, doesn't have to know about error.

Exceptions (bugs, programmer mistakes): we might be able to pass them automatically up the pipeline (as exceptions) — just a thought, don't know a good solution yet.

Active links (is this the only thing for which we might need context, btw?)

We could do something like <LinkProvider currentUrl="..." whatever="...">{renderRouteHandlers(...)}</LinkProvider> in react-renderer handler.

@rpominov
Copy link
Author

function createPipeline(...handlers) {
  handlers.reverse()
  return handlers.reduce((next, handler) => {
    var clear = () => {}
    var latestPayload
    return payload => {
      clear()
      latestPayload = payload
      clear = handler(payload, x => {
        if (latestPayload !== payload) {
          console.warn('calling next after clear')
          return
        }
        next(x)
      })
    }
  }, () => {})
}

function map(fn) {
  return (payload, next) => {
    next(fn(payload))
    return () => {}
  }
}

function repeat(payload, next) {
  var id = setInterval(() => {next(payload)}, 1000)
  return () => {clearInterval(id)}
}


// Usage
something.listen(createPipeline(
  map(x => x * 2), 
  repeat, 
  map(x => console.log(x))
))

@rpominov
Copy link
Author

foo.pipe().pipe() is a bad idea, because we have to care about foo1 = foo.pipe(bar); foo2 = foo1.pipe(baz1); foo3 = foo1.pipe(baz2) case. Better if we require that all handlers must be provided up front.

@rpominov
Copy link
Author

Clear :: () => void
Next :: payload => void
Stream :: Next => Clear | Rx.Stream | Bacon.Stream | Whatever.Stream...
Handler :: payload => Stream
createPipeline :: [Handler] => Next

@rpominov
Copy link
Author

function createPipeline(...handlers) {
  handlers.reverse()
  return handlers.reduce((next, handler) => {
    let dispose = () => {}
    return payload => {
      dispose()
      dispose = subscribe(handler(payload), next)
    }
  }, () => {})
}

function subscribe(stream, subscriber) {
  const dispose = stream(payload => {
    if (subscriber === null) {
      console.warn('calling next after clear')
      return
    }
    subscriber(payload)
  })
  return () => {
    subscriber = null
    dispose()
  }
}

@rpominov
Copy link
Author

/* Normaly we can subscribe to a stream by simply doing:
 *
 *   conts unsub = stream(callback)
 *
 * But we have to trust `stream` that it won't call the `callback`
 * after `unsub` has been called. This function fixes the issue.
 */
function subscribe(stream, callback) {
  const dispose = stream(payload => {
    if (callback === null) {
      console.warn('calling next after clear')
      return
    }
    callback(payload)
  })
  return () => {
    callback = null
    dispose()
  }
}

function flatMapLatest(stream, handler) {
  return sink => {
    let disposeSpawned = () => {}
    const disposeMain = subscribe(stream, payload => {
      disposeSpawned()
      disposeSpawned = subscribe(handler(payload), sink)
    })
    return () => {
      disposeSpawned()
      disposeMain()
    }
  }
}

function createPipeline(...handlers) {
  let _sink
  const initialStream = sink => {
    _sink = sink
    return () => {}
  }
  const resultStream = handlers.reduce(flatMapLatest, initialStream)
  resultStream(() => {})
  return _sink
}

@rpominov
Copy link
Author

@rpominov
Copy link
Author

Possible parts

@rpominov
Copy link
Author

rpominov commented May 4, 2016

  1. source → flatMapLatest → flatMapLatest → flatMapLatest → handler
  2. Event = Normal | Error | Redirect | End
  3. fmlNormal, fmlError, fmlRedirect, fmlEnd (?)
  4. on server source emits a Normal and an End, then we wait for End on other side and use the previous Event
  5. fml(a -> Stream<b,c,d> | Event<b,c,d>)

@rpominov
Copy link
Author

rpominov commented May 4, 2016

Event = {type: ... | 'END', payload: any}
Middleware = Event -> Event | Stream<Event> | Array<Event>

{type: 'END'} is special Event, we automatically dispose subscription after it. Other types like 'NORMAL', 'ERROR', 'REDIRECT', etc. are up to user (or maybe not, we might need a convention for example if we provide middlewares out of the box, but only END is special anyway)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment