MithrilJS / mithril.js

A JavaScript Framework for Building Brilliant Applications
https://mithril.js.org
MIT License
14.02k stars 925 forks source link

Stream Extras #2410

Open spacejack opened 5 years ago

spacejack commented 5 years ago

Description

This issue is to discuss potential additions and enhancements for mithril/stream.

This is a collection of helpers I've found useful working with Streams: https://github.com/spacejack/mithril-stream-extra It's a bit TypeScript-heavy because I wanted to add a ReadonlyStream type, but otherwise I think it's also got useful helpers for plain JS.

(Revisiting it however, I found the ReadonlyStream type is now broken with the current Typescript compiler... I'm having trouble coming up with a Stream-compatible read-only type so that ReadonlyStream types are usable with all Stream functions. This might need to be done as a complete custom stream .d.ts or brought into core to work.)

In addition to the above, these functions are for getting promises from streams:

/**
 * Promise that resolves on stream's initial value
 * Credit to @isiahmeadows for eliminating the extra map
 */
export function one<T>(s: ReadonlyStream<T>): Promise<T> {
    return new Promise<T>(resolve => {
        let done = false
        let s1: Stream<T>
        s1 = s.map(v => {
            if (done) {
                return
            }
            done = true
            if (s1 != null) {
                s1.end(true)
            }
            resolve(v)
        })
        if (done) {
            s1.end(true)
        }
    })
}

/**
 * Promise that resolves on stream's next value
 */
export function nextOne<T>(s: ReadonlyStream<T>): Promise<T> {
    return one(dropInitial(s))
}

Why

Having used streams in various ways, this is a set of helpers I've found most useful. I'd also like to hear from others about what additional stream functions or patterns they're using. And any suggestions/critiques or antipattern warnings about the above are welcome as well.

dead-claudia commented 5 years ago

Thought I'd give some initial feedback on each of your suggestions:

function dropInitial(s) {
    var ready = false
    return s.map(x => {
        if (ready) return x
        ready = true
        return Stream.HALT
    })
}

// This generalizes well
function dropInitial(s, n) {
    n = Math.floor(n)
    return s.map(x => {
        if (n === 0) return x
        n--
        return Stream.HALT
    })
}
var sentinel = {}
export function one(s) {
    return new Promise(function (resolve) {
        var child = sentinel
        var dep = s.map(function (v) {
            var memo = child
            child = undefined
            if (memo != null) {
                resolve(v)
                if (sentinel !== memo) memo.end(true)
            }
            return Stream.HALT
        })
        if (child == null) dep.end(true)
        else child = dep
    })
}

Edit: forgot to highlight a code block.

tzkmx commented 5 years ago

I'd like to see other operators as well, or having it easier to plug-in with an extension. For example I've followed the idea of @spacejack but borrowed inspiration from xstream/extras (same operator dropRepeats, but with an optional customEquality function)

So I came to a project with this addition: https://gist.github.com/tzkmx/d82789cabb1c635fc10f5233ace72171 ). I didn't know how to plugit into the custom library, so I've put my custom version of mithril-stream with my custom operator embedded in the source.

I'd like to see another operators added To, like groupBy, filter, etc. I know all of this can be custom implemented with map, but even then, it'd be nice to being able to publish contributions compatible with mithril-stream without forcing bloat into the goals of the project.

dead-claudia commented 5 years ago

@tzkmx To clarify, which do you mean by groupBy?

The first is somewhat straightforward and simple with ES6:

function groupBy(s, func) {
    s = s.map(v => [func(v), v)])
    let acc = new Map(), result = Stream(), end = false
    s.end.map(t => {
        if (!end && t === true) {
            end = true
            result(Array.from(acc))
            result.end(true)
        }
    })
    s.map(v => {
        if (!end) {
            let values = acc.get(key)
            if (values == null) acc.set(key, values = [])
            values.push(v)
        }
        return Stream.SKIP
    })
    return result
}

The second is much less simple, and it's complicated enough I'm not going to show the code for it here.

spacejack commented 5 years ago

@tzkmx your dropRepeats with a callback sounds like flyd's filter. flyd is the library I usually reference for ideas.