npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

super-mithril-stream

v0.0.0-next.22

Published

> ⚠️ Warning, this library is very new, and while it is does have a lot of tests, we plan to iterate a bit on the internals near term so do not use this in production yet. > If you still do decide to use it, make sure you pin to a specific version.

Downloads

119

Readme

super-mithril-stream

⚠️ Warning, this library is very new, and while it is does have a lot of tests, we plan to iterate a bit on the internals near term so do not use this in production yet. If you still do decide to use it, make sure you pin to a specific version.

A rewrite of mithril's stream library with more stuff.

  • Read only streams
  • Track stream creation / references
  • Less memory usage
  • Explicit get / set / update methods
  • Lots of new built in methods/operations
  • Compatible with Sin.js renderer
  • Better typescript support
  • Linear update with discrete transactions (Like S.js)

Quick Start

npm install super-mithril-stream
import Stream from 'super-mithril-stream'

let a = Stream(4)
let b = a.map( x => x * 2 )

a.get()
// 4

b.get()
// 8

a.set()

Why

At harth we already had our own fork of mithril-stream inlined into our codebase. This library aims to formalize these extensions.

More stuff 🎉

Read only streams

Any dependent stream is treated as a readonly stream both by typescript and at runtime.

Stream Tracking

You can manually track the creation and referencing of stream values within a function call. It is a bit like S.js or other similar signal libraries except the tracking is made explicit.

import Stream from 'super-mithril-stream'

let a = Stream(1)
let b = Stream(1)
let c = Stream(1)
let d = Stream(1)

let inner = new Set<Stream>()
let outer = new Set<Stream>()

let answer = Stream.track(() => {
    return c() + d() + Stream.track(() => {
        return a() + b()
    }, inner)
}, outer)

answer
// => 4

outer
// => Set(2) {c,d}

inner
// => Set(2) {a,b}

As you can see, the tracking contexts nest, and the inner contexts own the tracking of referenced streams exclusively. outer captured streams c and d because they were referenced in the outer track context. And inner captured streams a and b.

If you want tracking to cascade, you can share sets:

let answer = Stream.track(() => {
    return c() + d() + Stream.track(() => {
        return a() + b()
    }, sharedSet)
}, sharedSet)

This feature is deliberately low level to allow framework code to control exactly how tracking behaves external to the stream library. There is no atomic clock, or setTimeout considerations within super-mithril-stream so you can decide to model the tracking sets however you like.

You can also trackCreated and trackReferenced separately.

Because tracking is manual, you have a lot of power, e.g. you can pause and resume the same tracking set in between calls to await.

Less memory usage

This library's InternalStream is a class. For backwards compatiblity we do some black magic with setPrototypeOf to allow it to also act as a function getter/setter. But each instance of a stream is very cheap as all methods (and even some state) are inhertied from the prototype.

Explicit get / set / update methods

Sin.js compatibility

super-mithril-stream supports Sin.js observable protocol. This means you can use super-mithril-stream directly in sin's view and css definition inplace of s.live if you need a bit more builtin stream functionality or would just prefer the familiar interface of mithril-stream's API.

API

Extensions

Bacta streams are an API superset of mithril streams, but with some very slight changes in behaviour.

We've also added quite a few new methods to stream.

New accessors:

  • .get
  • .set
  • .update

New combinators:

  • .filter
  • .reject
  • .dropRepeats
  • .dropRepeatsWith
  • .debounce
  • .throttle

Tracking:

  • track
  • trackCreated
  • trackReferenced
  • sample
  • sampleCreated
  • sampleReferenced
  • untrack

New behaviour:

  • combined streams end when any dependency ends
  • .get returns a read only stream

stream.update

One minor frustation when working with mithril streams is incrementing values requires this little dance:

let count = v.useStream(0);

let inc = () => count(count() + 1);

We would like to provide this alternative:

let count = v.useStream(0);

let inc = () => count((x) => x + 1);

But this would prevent some very useful patterns where we pass a function into a stream (often used by a community favourite meiosis)

So we instead add an explicit update api which only accepts a visitor function, this allows us to increment a count like so:

let count = v.useStream(0);

let inc = () => count.update((x) => x + 1);

This more explicit API also ties in well with our new .get and .set stream APIs.

stream.get / stream.set

Streams can be read via the traditional getter / setter API used in mithril and flyd

const a = v.useStream(0);

a(); // => 0

a(1);

a(); // => 1

But we've found in practice it can be beneficial to be explicit when getting or setting. Lets say we have a list of streams and we want to turn it into a list of values:

let streams = [a, b, c, d];

// works fine
let values = streams.map((x) => x.get());

// works fine
let values = streams.map((x) => x());

// uh oh, accidentally updated the stream
let values = streams.map(x);

Another scenario: you want to let a component read from a stream, but not write to it.

// can only read
m(UntrustedComponent, { getValue: value.get });

// can read and write
m(TrustedComponent, { getValue: value });

stream.get is also its own stream, so you can subscribe to changes without having the ability to write back to the source.

someStream.get.map((x) => 
  console.log('someStream changed', x)
);

It is also just a lot easier to grep for, you can more easily distinguish an arbitrary function call from a stream get/set.

stream.filter

interface Filter<T> {
  (predicate: (value: T) => boolean): Stream<T>;
}

Only emits when the user provided predicate function returns a truthy value.

const person = useStream({ name: 'Barney', age: 15 });

setInterval(() => {
  person.update((x) => ({ ...x, age: x.age + 1 }));
}, 86400 * 365 * 1000);

const isAdult = (x) => x.age > 18;
const adult = person.filter(isAdult);

stream.reject

interface Reject<T> {
  (predicate: (value: T) => boolean): Stream<T>;
}

Only emits when the user provided predicate function returns a falsy value.

const child = person.reject(isAdult);

stream.dropRepeats and stream.dropRepeatsWith

Prevents stream emission if the new value is the same as the prior value. You can specify custom equality with dropRepeatsWith.

interface DropRepeats<T> {
  (): Stream<T>;
}
interface DropRepeatsWith<T> {
  (equality: (a: T, b: T) => boolean): Stream<T>;
}
// only send consecutively unique requests to the API 
const results = 
  searchValue.dropRepeats().awaitLatest(async (q) => {
    return m.request('/api/search?q=' + q);
  });

You can also specify equality:

const equality = (a, b) =>
  a.toLowerCase() == b.toLowerCase()

const results = 
  searchValue
    .dropRepeatsWith(equality)
    .awaitLatest(async (q) => {
      return m.request('/api/search?q=' + q);
    });

stream.afterSilence and stream.throttle

interface AfterSilence<T> {
  (ms?: number): Stream<T>;
}
interface Throttle<T> {
  (ms?: number): Stream<T>;
}
// hit the API at most every 300ms
const results1 = 
  searchValue
    .throttle(300)
    .awaitLatest(async (q) => {
      return m.request('/api/search?q=' + q);
    });

// hit the API when searchValue has not emitted any values for at least 300ms
const results2 = 
  searchValue
  .afterSilence(300)
  .awaitLatest(async (q) => {
    return m.request('/api/search?q=' + q);
  });

Compatibility with mithril-stream

Passes all mithril-stream tests (except for deliberate intentional behavioural changes)

Breaking changes from mithril-stream

Class methods

All methods on the stream instance are unbound class methods. This dramatically decreases memory usage but also means it is possible for a stream method to lose its this context if passed around as a first class function.

The only exception to this is the getter/setters:

  • stream()
  • stream(newValue)
  • stream.get()
  • stream.set(newValue)

All these functions are bound to the stream instance as it is a common pattern to use them as a callback directly.

Behaviour of combine/lift/merge

If any dependency stream ends it will also end any child streams including combinations of streams using operators such as combine, merge, lift etc.

mithril-stream will not end a combined stream if one of the dependencies ends.

In traditional mithril.js, if one of the dependencies of that combined stream ends, then the combined stream does not end. If other dependencies continue to emit the derived stream continues to emit as well.

This also makes component scoped streams much safer, as you can combine a local scoped stream with a global stream and know you won't have the derived stream continuing to emit once your component unmounts.

End stream propagation

In mithril-stream if you end a parent stream, the dependent streams don't end, The stream is unregistered from its parent but the end streams are never written to.

So you get stuff like this:

const a = m.stream(1)

const b = a.map( x => x * 2 )

a.end(true)

console.log(a.end(), b.end())
// true undefined

In practice b is ended because if you write to a, it won't propagate, and in theory if a and b are dereferenced they will be GC'd.

All the same super-mithril-stream will mark dependent streams as ended when a parent stream ends.

This let's you subscribe to .end on any stream in the dependency tree and it will reliably trigger.

We take this further with behaviour changes to merged/combined streams.

In mithril-stream, and flyd, if a dependency stream ends the combined stream doesn't necessarily end. This was picked up in @porsager's stream rewrite here

In Harth's usage of streams, we often scoped streams by having a single component stream that ends. This scoped stream was merged with all component created streams automatically guaranteeing a component's streams will be cleaned up when the component unmounts.

We highly associate stream ends with component lifecycles. A component never ends a stream it doesn't own, and a stream created by a component should always end when the component ends. If a component combines with a global stream we definitely don't want unmounted components to still have live combined streams emitting forever leading to memory leaks because they weren't explicitly ended.

We also feel it aligns better with related functionality in the wild e.g.

  • Many applicative librarys (e.g. include an Either.Left or Maybe.Nothing in a sequence or traverse and you wil get Left or Nothing even if oother dependencies are Just/Right )
  • Promise.all (if Rejected is compared to ended)
  • SQL null (compare null with any value and you get null).

There is no real correct answer to this: it is always up for interpretation. But in short, for this library, stream ending is contagious.

Some warnings removed

mithril-stream warns you if you do the wrong thing in a few places. E.g. if you use HALT instead of SKIP, or if you pass a non stream value to combine, or if you pass in a dependency that maybe is a stream but wasn't created by this library etc.

This library removes those warnings. The thinking is, if we are going to warn against using the library in undocumented ways then we'd have to write an infinite amount of warnings. We also now have the benefit of typescript which will provide the same sort of warning in each of these cases.

Also this library has no users, and mithril had to be more careful naturally.

Readonly streams

mithril-stream (like flyd) has no distinction between read/sink and write/source streams. super-mithril-stream explicitly forbids writing to a dependent stream.

Operations like map / filter etc all create a read only stream and will throw an error (and fail type checking) if you write to them.

const a = Stream()

a(1) // all good

const b = a.map( x => x * 2)

b(1) 
// Type error and
// throws an error

This means when you look at the definition const b = a.map( x => x * 2 ) you know with complete confidence that the value of b is defined by the expression: x * 2, and nothing can interfere with that.

Propagation

mithril-stream (like flyd) has a recursive update cycle. When you write to a stream, all its dependencies are updated, and the dependencies of those streams are updated and this all happens immediately and depth first.

super-mithril-stream instead behaves more like database transaction and other stream libraries like S.js.

Within this library, when you write to a stream, all its recursive dependencies are first gathered up without actually updating them.

Then all these dependencies are updated in a single pass, serially.

If any of these dependencies write to another stream, we schedule that update to happen at the end of the current update. This is all synchronous but these nested updates are treated as distinct transactions and are scheduled to happen in order.

This makes for far easier debugging when you have lots of nested updates and potential cyclic dependencies. Each write is scheduled as a discrete transaction, and you can watch all this happen in a single function in the source code instead of jumping through nested stream dispatches.

But because these transactions are scheduled, during an update, dependent read only values will not have the latest value until the update completes. The stream you write to will, and the streams that are written to within an update will also immediately have the latest value. But other streams have to wait their turn to update.

This shouldn't impact general usage unless if you are doing some very interesting checks to prevent infinite loops.

Here is an example:

const a = Stream(0)
const b = Stream(0)
a.map( x => {
  b( x * 2 )

  // at this point:
  // a = 1, 2
  // b = 2, 4
  // c = undefined, 4
})

let c = b.map(
  x => x * 2
)


a(1)
// a=1, b=2, c=4
a(2)
// a=2, b=4, c=8

We write to a, and this triggers a.map( ... ) to run. Within this map we write to b. Both these stream values will be immediately available. But the value of c will be undefined until b's transaction completes. In mithril-stream, writing to b would immediately trigger the update to c which would make the current value of c available immediately.

This is a definite trade off. We are favouring a more debuggable / traceable update cycle over architectural simplicity.

Browser / Node Compatibility

super-mithril-stream uses a lot of recent native features, and there is only an ESM build with no down compiling, so it may not work in all browsers.

We're happy to offer alternative builds if it is needed, but evergreen browsers have led to us to considering modern/native JS as a sensible default.