From 76e585c5236bf4049e69bc67ccb3835f8c3612d1 Mon Sep 17 00:00:00 2001 From: spacejack Date: Tue, 13 Nov 2018 18:55:45 -0500 Subject: [PATCH] Add Stream.lift (#1950) * Add stream.lift and tests * Add docs * Add to change-log --- docs/change-log.md | 5 ++ docs/stream.md | 44 +++++++++++++ stream/stream.js | 11 +++- stream/tests/test-stream.js | 121 ++++++++++++++++++++++++++++++++++++ 4 files changed, 180 insertions(+), 1 deletion(-) diff --git a/docs/change-log.md b/docs/change-log.md index b2893112..8f9449cf 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -45,6 +45,7 @@ - render/core: revamp the core diff engine, and introduce a longest-increasing-subsequence-based logic to minimize DOM operations when re-ordering keyed nodes. - API: Introduction of `m.prop()` ([#2268](https://github.com/MithrilJS/mithril.js/pull/2268)) - docs: Emphasize Closure Components for stateful components, use them for all stateful component examples. +- stream: Add `stream.lift` as a user-friendly alternative to `merge -> map` or `combine` [#1944](https://github.com/MithrilJS/mithril.js/issues/1944) #### Bug fixes @@ -123,6 +124,10 @@ - Fix IE bug where active element is null causing render function to throw error ([#1943](https://github.com/MithrilJS/mithril.js/pull/1943), [@JacksonJN](https://github.com/JacksonJN)) +#### Ospec improvements: + +- Log using util.inspect to show object content instead of "[object Object]" ([#1661](https://github.com/MithrilJS/mithril.js/issues/1661), [@porsager](https://github.com/porsager)) + --- ### v1.1.3 diff --git a/docs/stream.md b/docs/stream.md index 679294be..be78d0a8 100644 --- a/docs/stream.md +++ b/docs/stream.md @@ -7,6 +7,7 @@ - [Stream.merge](#streammerge) - [Stream.scan](#streamscan) - [Stream.scanMerge](#streamscanmerge) + - [Stream.lift](#streamlift) - [Stream.HALT](#streamhalt) - [Stream["fantasy-land/of"]](#streamfantasy-landof) - [Instance members](#instance-members) @@ -151,6 +152,37 @@ Argument | Type | Required | De --- +##### Stream.lift + +Creates a computed stream that reactively updates if any of its upstreams are updated. See [combining streams](#combining-streams). Unlike `combine`, the input streams are a variable number of arguments (instead of an array) and the callback receives the stream values instead of streams. There is no `changed` parameter. This is generally a more user-friendly function for applications than `combine`. + +`stream = Stream.lift(lifter, stream1, stream2, ...)` + +Argument | Type | Required | Description +------------ | --------------------------- | -------- | --- +`lifter` | `(any...) -> any` | Yes | See [lifter](#lifter) argument +`streams...` | list of `Streams` | Yes | Streams to be lifted +**returns** | `Stream` | | Returns a stream + +[How to read signatures](signatures.md) + +--- + +###### lifter + +Specifies how the value of a computed stream is generated. See [combining streams](#combining-streams) + +`any = lifter(streams...)` + +Argument | Type | Required | Description +------------ | -------------------- | -------- | --- +`streams...` | splat of `Streams` | No | Splat of zero or more streams that correspond to the streams passed to [`stream.lift`](#stream-lift) +**returns** | `any` | | Returns a computed value + +[How to read signatures](signatures.md) + +--- + ##### Stream.HALT A special value that can be returned to stream callbacks to halt execution of downstreams @@ -372,6 +404,18 @@ var greeting = stream.merge([a, b]).map(function(values) { console.log(greeting()) // logs "hello world" ``` +Or you can use the helper function `stream.lift()` + +```javascript +var a = stream("hello") +var b = stream("world") + +var greeting = stream.lift(function(_a, _b) { + return _a + " " + _b +}, a, b) + +console.log(greeting()) // logs "hello world" +``` There's also a lower level method called `stream.combine()` that exposes the stream themselves in the reactive computations for more advanced use cases diff --git a/stream/stream.js b/stream/stream.js index 9bf1fed5..d51520cb 100644 --- a/stream/stream.js +++ b/stream/stream.js @@ -63,7 +63,7 @@ function finalize(stream) { } function combine(fn, streams) { - if (!streams.every(valid)) throw new Error("Ensure that each item passed to stream.combine/stream.merge is a stream") + if (!streams.every(valid)) throw new Error("Ensure that each item passed to stream.combine/merge/lift is a stream") return initDependency(createStream(), streams, function() { return fn.apply(this, streams.concat([streams.filter(changed)])) }) @@ -148,11 +148,20 @@ function scanMerge(tuples, seed) { return newStream } +function lift() { + var fn = arguments[0] + var streams = Array.prototype.slice.call(arguments, 1) + return merge(streams).map(function(streams) { + return fn.apply(undefined, streams) + }) +} + createStream["fantasy-land/of"] = createStream createStream.merge = merge createStream.combine = combine createStream.scan = scan createStream.scanMerge = scanMerge +createStream.lift = lift createStream.HALT = HALT if (typeof module !== "undefined") module["exports"] = createStream diff --git a/stream/tests/test-stream.js b/stream/tests/test-stream.js index 69ca811d..2ef0ef94 100644 --- a/stream/tests/test-stream.js +++ b/stream/tests/test-stream.js @@ -200,6 +200,127 @@ o.spec("stream", function() { o(spy.callCount).equals(0) }) }) + o.spec("lift", function() { + o("transforms value", function() { + var stream = Stream() + var doubled = Stream.lift(function(s) {return s * 2}, stream) + + stream(2) + + o(doubled()).equals(4) + }) + o("transforms default value", function() { + var stream = Stream(2) + var doubled = Stream.lift(function(s) {return s * 2}, stream) + + o(doubled()).equals(4) + }) + o("transforms multiple values", function() { + var s1 = Stream() + var s2 = Stream() + var added = Stream.lift(function(s1, s2) {return s1 + s2}, s1, s2) + + s1(2) + s2(3) + + o(added()).equals(5) + }) + o("transforms multiple default values", function() { + var s1 = Stream(2) + var s2 = Stream(3) + var added = Stream.lift(function(s1, s2) {return s1 + s2}, s1, s2) + + o(added()).equals(5) + }) + o("transforms mixed default and late-bound values", function() { + var s1 = Stream(2) + var s2 = Stream() + var added = Stream.lift(function(s1, s2) {return s1 + s2}, s1, s2) + + s2(3) + + o(added()).equals(5) + }) + o("lifts atomically", function() { + var count = 0 + var a = Stream() + var b = Stream.lift(function(a) {return a * 2}, a) + var c = Stream.lift(function(a) {return a * a}, a) + var d = Stream.lift(function(b, c) { + count++ + return b + c + }, b, c) + + a(3) + + o(d()).equals(15) + o(count).equals(1) + }) + o("lifts default value atomically", function() { + var count = 0 + var a = Stream(3) + var b = Stream.lift(function(a) {return a * 2}, a) + var c = Stream.lift(function(a) {return a * a}, a) + var d = Stream.lift(function(b, c) { + count++ + return b + c + }, b, c) + + o(d()).equals(15) + o(count).equals(1) + }) + o("lift can return undefined", function() { + var a = Stream(1) + var b = Stream.lift(function() { + return undefined + }, a) + + o(b()).equals(undefined) + }) + o("lift can return stream", function() { + var a = Stream(1) + var b = Stream.lift(function() { + return Stream(2) + }, a) + + o(b()()).equals(2) + }) + o("lift can return pending stream", function() { + var a = Stream(1) + var b = Stream.lift(function() { + return Stream() + }, a) + + o(b()()).equals(undefined) + }) + o("lift can halt", function() { + var count = 0 + var a = Stream(1) + var b = Stream.lift(function() { + return Stream.HALT + }, a)["fantasy-land/map"](function() { + count++ + return 1 + }) + + o(b()).equals(undefined) + o(count).equals(0) + }) + o("lift will throw with a helpful error if given non-stream values", function () { + var spy = o.spy() + var a = Stream(1) + var thrown = null; + try { + Stream.lift(spy, a, "") + } catch (e) { + thrown = e + } + + o(thrown).notEquals(null) + o(thrown.constructor === TypeError).equals(false) + o(spy.callCount).equals(0) + }) + }) o.spec("merge", function() { o("transforms an array of streams to an array of values", function() { var all = Stream.merge([