paldepind / flyd

The minimalistic but powerful, modular, functional reactive programming library in JavaScript.
MIT License
1.56k stars 84 forks source link

Enhancement: Add Readonly-Streams (at least in Typings) #212

Open Najros opened 3 years ago

Najros commented 3 years ago

Currently there is no possibility to export a stream as readonly, to prevent the updating of a combined, piped or mapped stream. At least some typescript typings could enhance the proposed usage api doc for such an exported stream.

For example:

const x = stream(5)
const squareX = x.pipe(map(x => x*x))

// squareX should not be writable!
squareX() // ok
squareX(9) // not ok!
x(3) // this is ok, instead
nordfjord commented 3 years ago

I think I understand what you want. You want the types to be something like:

flyd.stream(v: T): WritableStream<T>

type Stream<T> = WritableStream<T> | ReadonlyStream<T>

map<T,V>(fn: (v: T)=> V) => (s: Stream<T>) => ReadonlyStream<V>

Did you run into a problem putting values into dependent streams where these types would've helped? What was the situation?

Najros commented 3 years ago

No, it is not a technical issue. It is all about a clear interface: if i expose a dependent stream, I do not want that anyone can put values in it.

Currently I created my own ReadonlyStream interface by copying the Stream interface and removing the unwanted methods (the setters and end stream)

export interface IReadonlyStream<T> {
  (): T

  map<V>(project: (value: T) => V): flyd.Stream<V>
  ap<A, B>(this: flyd.Stream<(value: A) => B>, stream: flyd.Stream<A>): flyd.Stream<B>
  chain<V>(project: (value: T) => flyd.Stream<V>): flyd.Stream<V>
  of<V>(...values: V[]): flyd.Stream<V>

  pipe<V>(operator: (input: flyd.Stream<T>) => flyd.Stream<V>): flyd.Stream<V>

  ["fantasy-land/map"]<V>(project: (value: T) => V): flyd.Stream<V>
  ["fantasy-land/ap"]<V>(fn: (value: flyd.Stream<T>) => V): flyd.Stream<V>
  ["fantasy-land/chain"]<V>(project: (value: T) => flyd.Stream<V>): flyd.Stream<V>
  ["fantasy-land/of"]<V>(...values: V[]): flyd.Stream<V>

  val: T
  hasVal: boolean
}

and I am doing an explicit cast like:

const squareX = x.pipe(map(x => x*x)) as IReadonlyStream<number>

But some methods have problems with that interface and then I need to do a cast back to flyd.Stream. (i.e. lift() does not accept ReadonlyStreams and the result can not even be casted easy to ReadonlyStream) In the end, I am doing a lot of castings and that is bad code style.

On the other hand, I am not sure, if ReadonlyStreams are a wanted feature in general, or if it is only a habit of mine. What is the use of dependent streams being writeable?

Cheers and thanks for the quick response.

PS: Maybe I can rework the typings in such a way and contribute it back to the project? I realy love flyd and use it as my state layer (as redux replacement) together with preact. That looks like this:

export default function WeekHeader(): VNode {
  const start = useStream(SCurrentWeek) // SCurrentWeek is a ReadonlyStream that depends on SCurrentDate
  const thisWeek = useStream(SThisWeek)
  const setCurrent = useStreamUpdater(SCurrentDate) // a stream updater accepts a callback: (current) => next
  const thisBtn = (
    <button className="btn green" onClick={() => setCurrent(new Date())} disabled={thisWeek == start}>
      <i className="material-icons">adjust</i>
    </button>
  )
  return (
    <div className="header">
      <button className="btn" onClick={() => setCurrent(addWeeks(-1))}>
        <i className="material-icons">chevron_left</i>
      </button>
      {thisBtn}
      <button className="btn btn-flat" onClick={() => setCurrent(addMonths(-1))}>
        <i className="material-icons">chevron_left</i>
      </button>
      <button className="btn btn-flat" onClick={() => setCurrent(addMonths(1))}>
        <i className="material-icons">chevron_right</i>
      </button>
      <div className="title">
        <h4 className="month">{format(start, "LLLL", localeOptions)}</h4>
        <h4>&nbsp;</h4>
        <h4 className="year">{format(start, "yyyy")}</h4>
      </div>
      <button className="btn btn-flat" onClick={() => setCurrent(addYears(-1))}>
        <i className="material-icons">chevron_left</i>
      </button>
      <button className="btn btn-flat" onClick={() => setCurrent(addYears(1))}>
        <i className="material-icons">chevron_right</i>
      </button>
      {thisBtn}
      <button className="btn" onClick={() => setCurrent(addWeeks(1))}>
        <i className="material-icons">chevron_right</i>
      </button>
    </div>
  )
}
nordfjord commented 3 years ago

Hey again!

Maybe I can rework the typings in such a way and contribute it back to the project?

Absolutely! Feel free to submit a PR with reworked/better typings

What is the use of dependent streams being writeable?

It's less of a use case and more of "that's how it works internally."

I have however used it in one instance where data could come from multiple sources. I.e. a user entity had a feature flags object, but I also had a live subscription to feature flag updates

const user$ = stream()
const featureFlags$ = user$.map(prop('featureFlags'))

fetch('/user').then(x => x.json()).then(user$)
subscribeToFeatureFlagUpdates(featureFlags$)

copying the Stream interface and removing the unwanted methods (the setters and end stream)

I would definitely keep the end stream on there. There are cases where you'd want to end a dependent stream separately from its parent.

const x$ = stream(3)
const xx$ = x$.map(x => x * x)
const xxx$ = xx$.map(xx => xx * xx)

// does not end the parent stream, but does end the xxx$ stream
xx$.end(true)

I realy love flyd and use it as my state layer (as redux replacement) together with preact.

Nice! That's exactly how I started using it. albeit with mithril as the view layer.

check out @foxdonut 's meiosis for inspiration on stream based state management https://meiosis.js.org/