simplify streams, lint docs
This commit is contained in:
parent
a7f8018df9
commit
6ce2a384ec
20 changed files with 1271 additions and 2132 deletions
|
|
@ -12,9 +12,10 @@
|
|||
- [m.jsonp](jsonp.md)
|
||||
- [m.parseQueryString](parseQueryString.md)
|
||||
- [m.buildQueryString](buildQueryString.md)
|
||||
- [m.prop](prop.md)
|
||||
- [m.withAttr](withAttr.md)
|
||||
- [m.trust](trust.md)
|
||||
- [m.fragment](fragment.md)
|
||||
- [m.redraw](redraw.md)
|
||||
- [m.version](version.md)
|
||||
|
||||
- [stream](stream.md)
|
||||
|
|
@ -38,7 +38,7 @@ JSON-P has several limitations: it can only use GET requests, it implicitly trus
|
|||
Some services follow the de-facto convention of responding with JSON-P if a `callback` querystring key is provided, thus making `m.jsonp` automatically work without any effort:
|
||||
|
||||
```javascript
|
||||
m.jsonp({url: "https://api.github.com/users/lhorie"}).run(function(response) {
|
||||
m.jsonp({url: "https://api.github.com/users/lhorie"}).then(function(response) {
|
||||
console.log(response.data.login) // logs "lhorie"
|
||||
})
|
||||
```
|
||||
|
|
@ -50,7 +50,7 @@ m.jsonp({
|
|||
url: "https://api.flickr.com/services/feeds/photos_public.gne?tags=kitten&format=json",
|
||||
callbackKey: "jsoncallback",
|
||||
})
|
||||
.run(function(response) {
|
||||
.then(function(response) {
|
||||
console.log(response.link) // logs "https://www.flickr.com/photos/tags/kitten/"
|
||||
})
|
||||
```
|
||||
|
|
@ -63,7 +63,7 @@ m.jsonp({
|
|||
url: "https://api.github.com/users/lhorie",
|
||||
callbackName: "__callback",
|
||||
})
|
||||
.run(function(response) {
|
||||
.then(function(response) {
|
||||
console.log(response.data.login) // logs "lhorie"
|
||||
})
|
||||
```
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ Typically, redrawing from an `oninit` or `onbeforeupdate` hook is meaningless si
|
|||
|
||||
```javascript
|
||||
// AVOID
|
||||
var greeting = m.prop("")
|
||||
var greeting = ""
|
||||
|
||||
var BrokenComponent = {
|
||||
onupdate: function() {
|
||||
|
|
@ -208,16 +208,16 @@ var BrokenComponent = {
|
|||
m.redraw()
|
||||
},
|
||||
view: function() {
|
||||
return m("div[title=Hello]", {onclick: m.withAttr("title", greeting)}, this.greeting)
|
||||
return m("div[title=Hello]", {onclick: m.withAttr("title", function(v) {greeting = v})}, this.greeting)
|
||||
}
|
||||
}
|
||||
|
||||
// PREFER
|
||||
var greeting = m.prop("")
|
||||
var greeting = ""
|
||||
|
||||
var WorkingComponent = {
|
||||
view: function() {
|
||||
return m("div[title=Hello]", {onclick: m.withAttr("title", greeting)}, greeting())
|
||||
return m("div[title=Hello]", {onclick: m.withAttr("title", function(v) {greeting = v})}, greeting)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
43
docs/lint.js
43
docs/lint.js
|
|
@ -44,8 +44,11 @@ function ensureCodeIsRunnable(file, data) {
|
|||
}
|
||||
|
||||
try {
|
||||
initMocks()
|
||||
new Function("console,fetch,module,require", code).call(this, silentConsole, fetch, {exports: {}}, function(dep) {
|
||||
if (dep.indexOf("./mycomponent") === 0) return {view: function() {}}
|
||||
if (dep.indexOf("mithril/ospec/ospec") === 0) return global.o
|
||||
if (dep.indexOf("mithril/stream") === 0) return global.stream
|
||||
if (dep === "mithril") return global.m
|
||||
})
|
||||
}
|
||||
|
|
@ -61,6 +64,39 @@ function ensureCommentStyle(file, data) {
|
|||
})
|
||||
}
|
||||
|
||||
function initMocks() {
|
||||
global.window = require("../test-utils/browserMock")()
|
||||
global.document = window.document
|
||||
global.m = require("../index")
|
||||
global.o = require("../ospec/ospec")
|
||||
global.stream = require("../stream")
|
||||
|
||||
//routes consumed by request.md
|
||||
global.window.$defineRoutes({
|
||||
"GET /api/v1/users": function(request) {
|
||||
return {status: 200, responseText: JSON.stringify([{name: ""}])}
|
||||
},
|
||||
"GET /api/v1/todos": function(request) {
|
||||
return {status: 200, responseText: JSON.stringify([])}
|
||||
},
|
||||
"POST /api/v1/upload": function(request) {
|
||||
return {status: 200, responseText: JSON.stringify([])}
|
||||
},
|
||||
"GET /files/icon.svg": function(request) {
|
||||
return {status: 200, responseText: "<svg></svg>"}
|
||||
},
|
||||
"GET /files/data.csv": function(request) {
|
||||
return {status: 200, responseText: "a,b,c"}
|
||||
},
|
||||
"GET /api/v1/users/123": function(request) {
|
||||
return {status: 200, responseText: JSON.stringify({id: 123})}
|
||||
},
|
||||
"GET /api/v1/users/foo:bar": function(request) {
|
||||
return {status: 200, responseText: JSON.stringify({id: 123})}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
//runner
|
||||
function traverseDirectory(pathname, callback) {
|
||||
pathname = pathname.replace(/\\/g, "/")
|
||||
|
|
@ -87,14 +123,9 @@ function traverseDirectory(pathname, callback) {
|
|||
})
|
||||
}
|
||||
|
||||
//init mocks
|
||||
global.window = require("../test-utils/browserMock")()
|
||||
global.document = window.document
|
||||
global.m = require("../index")
|
||||
|
||||
//run
|
||||
traverseDirectory("./docs", function(pathname) {
|
||||
if (pathname.indexOf(".md") > -1 && pathname.indexOf("migration") < 0 && pathname.indexOf("route") < 0) {
|
||||
if (pathname.indexOf(".md") > -1 && pathname.indexOf("migration") < 0 && pathname.indexOf("tutorial") < 0) {
|
||||
fs.readFile(pathname, "utf8", function(err, data) {
|
||||
if (err) console.log(err)
|
||||
else lint(pathname, data)
|
||||
|
|
|
|||
765
docs/prop.md
765
docs/prop.md
|
|
@ -1,765 +0,0 @@
|
|||
# prop()
|
||||
|
||||
- [API](#api)
|
||||
- [Static members](#static-members)
|
||||
- [prop.combine](#propcombine)
|
||||
- [prop.reject](#propreject)
|
||||
- [prop.merge](#propmerge)
|
||||
- [prop.HALT](#prophalt)
|
||||
- [prop["fantasy-land/of"]](#propfantasy-landof)
|
||||
- [Instance members](#static-members)
|
||||
- [stream.run](#streamrun)
|
||||
- [stream.end](#streamend)
|
||||
- [stream.error](#streamerror)
|
||||
- [stream.catch](#streamcatch)
|
||||
- [stream["fantasy-land/of"]](#streamfantasy-landof)
|
||||
- [stream["fantasy-land/map"]](#streamfantasy-landmap)
|
||||
- [stream["fantasy-land/ap"]](#streamfantasy-landap)
|
||||
- [Basic usage](#basic-usage)
|
||||
- [Streams as variables](#streams-as-variables)
|
||||
- [Bidirectional bindings](#bidirectional-bindings)
|
||||
- [Computed properties](#computed-properties)
|
||||
- [Loading icons and error messages](#loading-icons-and-error-messages)
|
||||
- [Streams vs promises](#streams-vs-promises)
|
||||
- [Chaining streams](#chaining-streams)
|
||||
- [Combining streams](#combining-streams)
|
||||
- [Absorbing streams](#absorbing-streams)
|
||||
- [Stream states](#stream-states)
|
||||
- [Handling errors](#handling-errors)
|
||||
- [Serializing streams](#serializing-streams)
|
||||
- [Streams do not trigger rendering](#streams-do-not-trigger-rendering)
|
||||
- [What is Fantasy Land](#what-is-fantasy-land)
|
||||
|
||||
---
|
||||
|
||||
### API
|
||||
|
||||
Creates a stream
|
||||
|
||||
`stream = m.prop(value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the prop is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
#### Static members
|
||||
|
||||
##### prop.combine
|
||||
|
||||
Creates a computed stream that reactively updates if any of its upstreams are updated. See [combining streams](#combining-streams)
|
||||
|
||||
`stream = m.prop.combine(combiner, streams)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | --------------------------- | -------- | ---
|
||||
`combiner` | `(Stream..., Array) -> any` | Yes | See [combiner](#combiner) argument
|
||||
`streams` | `Array<Stream>` | Yes | A list of streams to be combined
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
###### combiner
|
||||
|
||||
Specifies how the value of a computed stream is generated. See [combining streams](#combining-streams)
|
||||
|
||||
`any = combiner(streams..., changed)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`streams...` | splat of `Stream`s | No | Splat of zero or more streams that correspond to the streams passed as the second argument to [`prop.combine`](#prop-combine.md)
|
||||
`changed` | `Array<Stream>` | Yes | List of streams that were affected by an update
|
||||
**returns** | `any` | | Returns a computed value
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### prop.reject
|
||||
|
||||
Creates a stream in a error state. See [stream states](#stream-states)
|
||||
|
||||
`stream = m.prop.reject(value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`value` | `any` | Yes | The error value
|
||||
**returns** | `Stream` | | Returns a stream in an error state
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### prop.merge
|
||||
|
||||
Creates a stream whose value is the array of values from an array of streams
|
||||
|
||||
`stream = m.prop.merge(streams)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`streams` | `Array<Stream>` | Yes | A list of streams
|
||||
**returns** | `Stream` | | Returns a stream whose value is an array of input stream values
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### prop.HALT
|
||||
|
||||
A special value that can be returned to stream callbacks to halt execution of downstreams
|
||||
|
||||
---
|
||||
|
||||
##### prop["fantasy-land/of"]
|
||||
|
||||
This method is functionally identical to `m.prop`. It exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = m.prop["fantasy-land/of"](value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the prop is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
#### Instance members
|
||||
|
||||
##### stream.run
|
||||
|
||||
Creates a dependent stream whose value is set to the result of the callback function. See [chaining streams](#chaining-streams)
|
||||
|
||||
If the callback returns a stream, it is absorbed, and `dependentStream` adopts its value and state.
|
||||
|
||||
`dependentStream = m.prop().run(callback)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`callback` | `any -> any` | Yes | A callback whose return value becomes the value of the stream
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream.end
|
||||
|
||||
A co-dependent stream that unregisters dependent streams when set to true. See [ended state](#ended-state).
|
||||
|
||||
`endStream = m.prop().end`
|
||||
|
||||
---
|
||||
|
||||
##### stream.error
|
||||
|
||||
A co-dependent stream that is set if the stream is in an errored state. See [handling errors](#handling-errors).
|
||||
|
||||
`errorStream = m.prop().error`
|
||||
|
||||
---
|
||||
|
||||
##### stream.catch
|
||||
|
||||
Returns an active stream whose value is equal to the return value of `catch`'s callback.
|
||||
|
||||
`stream = m.prop().catch(callback)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`callback` | `any -> any` | Yes | A callback whose return value becomes the value of the stream returned by `catch`
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/of"]
|
||||
|
||||
This method is functionally identical to `m.prop`. It exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = m.prop()["fantasy-land/of"](value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the prop is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/map"]
|
||||
|
||||
Creates a dependent stream whose value is set to the result of the callback function. See [chaining streams](#chaining-streams)
|
||||
|
||||
This method is almost functionally identical to [`stream.run()`](#stream-run), except that if the return value is a stream, the stream is not absorbed.
|
||||
|
||||
This method exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`dependentStream = m.prop()["fantasy-land/of"](callback)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`callback` | `any -> any` | Yes | A callback whose return value becomes the value of the stream
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/ap"]
|
||||
|
||||
The name of this method stands for `apply`. If a stream `a` has a function as its value, another stream `b` can use it as the argument to `b.ap(a)`. Calling `ap` will call the function with the value of stream `b` as its argument, and it will return another stream whose value is the result of the function call. This method exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = m.prop()["fantasy-land/ap"](apply)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`apply` | `Stream` | Yes | A stream whose value is a function
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
### Basic usage
|
||||
|
||||
#### Streams as variables
|
||||
|
||||
`m.prop()` returns a stream. At its most basic level, a stream works similar to a variable or a getter-setter property: it can hold state, which can be modified.
|
||||
|
||||
```javascript
|
||||
var username = m.prop("John")
|
||||
console.log(username()) // logs "John"
|
||||
|
||||
username("John Doe")
|
||||
console.log(username()) // logs "John Doe"
|
||||
```
|
||||
|
||||
The main difference is that a stream is a function, and therefore can be composed into higher order functions.
|
||||
|
||||
```javascript
|
||||
var users = m.prop()
|
||||
|
||||
// request users from a server using the fetch API
|
||||
fetch("/api/users")
|
||||
.then(function(response) {return response.json()})
|
||||
.then(users)
|
||||
```
|
||||
|
||||
In the example above, the `users` stream is populated with the response data when the request resolves.
|
||||
|
||||
#### Bidirectional bindings
|
||||
|
||||
Streams can also be populated from other higher order functions, such as [`m.withAttr`](withAttr.md)
|
||||
|
||||
```javascript
|
||||
// a stream
|
||||
var user = m.prop("")
|
||||
|
||||
// a bi-directional binding to the stream
|
||||
m("input", {
|
||||
oninput: m.withAttr("value", user),
|
||||
value: user()
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, when the user types in the input, the `user` stream is updated to the value of the input field.
|
||||
|
||||
#### Computed properties
|
||||
|
||||
Streams are useful for implementing computed properties:
|
||||
|
||||
```javascript
|
||||
var title = m.prop("")
|
||||
var slug = title.run(function(value) {
|
||||
return value.toLowerCase().replace(/\W/g, "-")
|
||||
})
|
||||
|
||||
title("Hello world")
|
||||
console.log(slug()) // logs "hello-world"
|
||||
```
|
||||
|
||||
In the example above, the value of `slug` is computed when `title` is updated, not when `slug` is read.
|
||||
|
||||
It's of course also possible to compute properties based on multiple streams:
|
||||
|
||||
```javascript
|
||||
var firstName = m.prop("John")
|
||||
var lastName = m.prop("Doe")
|
||||
var fullName = m.prop.combine(function(first, last) {
|
||||
return first() + " " + last()
|
||||
}, [firstName, lastName])
|
||||
|
||||
firstName("Mary")
|
||||
|
||||
console.log(fullName()) // logs "Mary Doe"
|
||||
```
|
||||
|
||||
Computed properties in Mithril are updated atomically: the callback to `combine` will never be called more than once per value update, no matter how complex the computed property's dependency graph is.
|
||||
|
||||
#### Loading icons and error messages
|
||||
|
||||
Here's an example using [`m.request`](request.md) that uses streams to implement a loading indicator and an error message for an AJAX call:
|
||||
|
||||
```javascript
|
||||
var RobustExample = {
|
||||
oninit: function() {
|
||||
this.data = m.request({
|
||||
method: "GET",
|
||||
url: "/api/items",
|
||||
})
|
||||
},
|
||||
view: function() {
|
||||
return [
|
||||
this.data.error() ? [
|
||||
m(".error", this.data.error().message)
|
||||
] : this.data() ? [
|
||||
this.data().map(item => m("div", item.name))
|
||||
] : m(".loading-icon")
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
m.route(document.body, "/", {
|
||||
"/": RobustExample
|
||||
})
|
||||
```
|
||||
|
||||
When this component is initialized, `m.request` is called and its return value is assigned to `this.data`. Before the request completes, that stream remains in a pending state, and therefore has a value of `undefined`. `this.data.error` is the error stream for the request. Since the stream is pending, its error stream also remain in a pending state, with a value of `undefined`.
|
||||
|
||||
Before the request completes, the component renders once. Both `this.data` and `this.data.error` return `undefined` at this point, so the ternary operators fall through to `m(".loading-icon")`, which in turn creates a loading icon element in the DOM.
|
||||
|
||||
When the request to the server completes, `this.data` is populated with the response data, `this.data.error` is set to an active state (but still with a value of `undefined`), and the component is re-rendered. The `view` function returns a list of vnodes containing item names, and therefore the loading icon is replaced by a list of `div` elements are created in the DOM.
|
||||
|
||||
If the request to the server fails, `this.data` is set to undefined and `this.data.error` is populated with the error. Therefore, `view` returns `[m(".error", this.data.error().message)]`, which replaces the loading icon with the error message in the DOM.
|
||||
|
||||
To clear the error message, you can set the value of the `this.data` to an empty array, or set `this.data.error` to `undefined`.
|
||||
|
||||
---
|
||||
|
||||
### Streams vs promises
|
||||
|
||||
Mithril streams have some similarities to [ES6 promises](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise):
|
||||
|
||||
- streams can be [chained](#chaining-streams) (analogous to `promise.then(callback)`)
|
||||
- streams can [absorb other streams](#absorbing-streams) (analogous to `promise.then(function() {return Promise.resolve(1)})`)
|
||||
- streams have [composable error handling semantics](#handling-errors) (analogous to `promise.catch`)
|
||||
|
||||
These semantic similarities are designed to make it easy to migrate from promise-based asynchronous code to stream-based code.
|
||||
|
||||
For example, here's some sample promise-based code:
|
||||
|
||||
```javascript
|
||||
fetch("/api/users", {method: "GET"}).then(function(response) {return response.json()})
|
||||
.then(function(users) {
|
||||
if (users.length === 0) return Promise.reject("No users found")
|
||||
})
|
||||
.catch(function(e) {
|
||||
console.log(e)
|
||||
})
|
||||
```
|
||||
|
||||
And here's equivalent stream-based code:
|
||||
|
||||
```javascript
|
||||
m.request({url: "/api/users", method: "GET"})
|
||||
.run(function(users) {
|
||||
if (users.length === 0) return m.prop.reject("No users found")
|
||||
})
|
||||
.catch(function(e) {
|
||||
console.log(e)
|
||||
})
|
||||
```
|
||||
|
||||
Aside from the syntax differences between the [`fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and [`m.request()`](request.md) in the first line of each snippet above, the only other syntax difference is that streams use the method `.run()` to chain, instead of `.then()`.
|
||||
|
||||
#### Differences
|
||||
|
||||
In most use cases, streams can be used as replacements for promises without much effort, by simply renaming `.then()` to `.run()`.
|
||||
|
||||
One major difference that can affect a migration is that `.run()` only accepts one argument (whereas `.then()` accepts an error handler as the second argument. Misplacement of error handlers is a common source of bugs in promise-based code, and it's generally recommended that error handlers be attached using `.catch()` rather than passed as a second argument to `.then()`. To avoid those issues, error handlers in streams can only be defined using the `.catch()` method.
|
||||
|
||||
Another more obscure functional difference is that if a promise is passed as an argument to `Promise.resolve()` and `Promise.reject()`, the promise is absorbed, whereas absorption does not occur in their stream counterparts `m.prop()` and `m.prop.reject()`.
|
||||
|
||||
There are also a few important differences in semantics between promises and streams. A promise can only ever resolve to one value. Streams, on the other hand, are *reactive*: a stream's value can be changed freely, and it automatically updates the values of other streams that depend on it.
|
||||
|
||||
Promises are required by spec to resolve asynchronously, even if the resolution value is known in advance (e.g. `Promise.resolve("hello")`). Mithril streams are guaranteed to update synchronously and atomically.
|
||||
|
||||
Mithril streams are also more oriented towards functional programming. In addition to being usable for composing higher order functions, the stream API comply with [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land), which enables interoperability with functional libraries like Ramda and Sanctuary.
|
||||
|
||||
#### Interoperability with promises
|
||||
|
||||
An increasing number of third party APIs return promises, and it's often desirable to transfer their resolved values to Mithril streams. This can be accomplished by simply chaining the stream itself to the promise chain:
|
||||
|
||||
```javascript
|
||||
var promise = Promise.resolve(123)
|
||||
var stream = m.prop()
|
||||
|
||||
// set the stream to listen to the promise resolution event
|
||||
promise.then(stream)
|
||||
```
|
||||
|
||||
To track promise rejections as well as resolutions, pass the error stream as a rejection callback:
|
||||
|
||||
```javascript
|
||||
promise.then(stream, stream.error)
|
||||
```
|
||||
|
||||
To use a stream value to resolve a promise, simply pass the stream value to it:
|
||||
|
||||
```javascript
|
||||
var stream = m.prop("hello")
|
||||
var promise = Promise.resolve(stream())
|
||||
// promise resolves to "hello"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Chaining streams
|
||||
|
||||
Streams can be chained using the `run` method. A chained stream is also known as a *dependent stream*.
|
||||
|
||||
```javascript
|
||||
// parent stream
|
||||
var stream = m.prop(1)
|
||||
|
||||
// dependent stream
|
||||
var doubled = stream.run(function(value) {
|
||||
return value * 2
|
||||
})
|
||||
|
||||
console.log(doubled()) // logs 2
|
||||
```
|
||||
|
||||
Dependent streams are *reactive*: their values are updated any time the value of their parent stream is updated. This happens regardless of whether the dependent stream was created before or after the value of the parent stream was set.
|
||||
|
||||
You can prevent dependent streams from being updated by returning the special value `m.prop.HALT`
|
||||
|
||||
```javascript
|
||||
var halted = m.prop(1).run(function(value) {
|
||||
return m.prop.HALT
|
||||
})
|
||||
|
||||
halted.run(function() {
|
||||
// never runs
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Combining streams
|
||||
|
||||
Streams can depend on more than one parent stream. These kinds of streams can be created via `m.prop.combine()`
|
||||
|
||||
```javascript
|
||||
var a = m.prop(5)
|
||||
var b = m.prop(7)
|
||||
|
||||
var added = m.prop.combine(function(a, b) {
|
||||
return a() + b()
|
||||
}, [a, b])
|
||||
|
||||
console.log(added()) // logs 12
|
||||
```
|
||||
|
||||
A stream can depend on any number of streams and it's guaranteed to update atomically. For example, if a stream A has two dependent streams B and C, and a fourth stream D is dependent on both B and C, the stream D will only update once if the value of A changes. This guarantees that the callback for stream D is never called with unstable values such as when B has a new value but C has the old value. Atomicity also bring the performance benefits of not recomputing downstreams unnecessarily.
|
||||
|
||||
You can prevent dependent streams from being updated by returning the special value `m.prop.HALT`
|
||||
|
||||
```javascript
|
||||
var halted = m.prop.combine(function(stream) {
|
||||
return m.prop.HALT
|
||||
}, [m.prop(1)])
|
||||
|
||||
halted.run(function() {
|
||||
// never runs
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Absorbing streams
|
||||
|
||||
Similar to promises, stream can absorb other streams. Returning a stream from the callback to `.run()` or `.catch()` will cause the wrapper stream to *absorb* the inner stream and adopt its value and [state](#stream-states):
|
||||
|
||||
```javascript
|
||||
var stream = m.prop()
|
||||
var mapped = m.prop(1).run(function(value) {
|
||||
return stream(value * 2)
|
||||
})
|
||||
|
||||
console.log(mapped()) // logs 2
|
||||
|
||||
stream(4)
|
||||
|
||||
console.log(mapped()) // logs 4
|
||||
```
|
||||
|
||||
```javascript
|
||||
var mapped = m.prop.reject(new Error("error")).catch(function(e) {
|
||||
return m.prop(2)
|
||||
})
|
||||
|
||||
console.log(mapped()) // logs 2
|
||||
```
|
||||
|
||||
Stream absorption does not occur in fantasy-land methods (i.e. `["fantasy-land/map"]()`, `["fantasy-land/ap"]()`, `["fantasy-land/of"]()`)
|
||||
|
||||
---
|
||||
|
||||
### Stream states
|
||||
|
||||
At any given time, a stream can be in one of four states: *pending*, *active*, *errored* and *ended*.
|
||||
|
||||
#### Pending state
|
||||
|
||||
Pending streams can be created by calling `m.prop()` with no parameters.
|
||||
|
||||
```javascript
|
||||
var pending = m.prop()
|
||||
```
|
||||
|
||||
If a stream is dependent on more than one stream, any of its parent streams is in a pending state, the dependent streams is also in a pending state, and does not update its value.
|
||||
|
||||
```javascript
|
||||
var a = m.prop(5)
|
||||
var b = m.prop() // pending stream
|
||||
|
||||
var added = m.prop.combine(function(a, b) {
|
||||
return a() + b()
|
||||
}, [a, b])
|
||||
|
||||
console.log(added()) // logs undefined
|
||||
```
|
||||
|
||||
In the example above, `added` is a pending stream, because its parent `b` is also pending.
|
||||
|
||||
This also applies to dependent streams created via `stream.run`:
|
||||
|
||||
```javascript
|
||||
var stream = m.prop()
|
||||
var doubled = stream.run(function(value) {return value * 2})
|
||||
|
||||
console.log(doubled()) // logs undefined because `doubled` is pending
|
||||
```
|
||||
|
||||
#### Active state
|
||||
|
||||
When a stream receives a value, it becomes active (unless the stream is ended).
|
||||
|
||||
```javascript
|
||||
var stream1 = m.prop("hello") // stream1 is active
|
||||
|
||||
var stream2 = m.prop() // stream2 starts off pending
|
||||
stream2("world") // then becomes active
|
||||
```
|
||||
|
||||
A dependent stream with multiple parents becomes active if all of its parents are active.
|
||||
|
||||
In the example above, setting `b(7)` would cause `b` to become active, and therefore `added` would also become active, and be updated to have the value `12`
|
||||
|
||||
#### Errored state
|
||||
|
||||
Errored streams can be created by calling `m.prop.reject()`
|
||||
|
||||
```javascript
|
||||
var erroredStream = m.prop.reject(new Error("Server is offline"))
|
||||
```
|
||||
|
||||
A stream can also become errored if it's a dependent stream and its [`combiner`](#combiner) or [`run`](#stream-run) function throws an error
|
||||
|
||||
```javascript
|
||||
var errored1 = m.prop(1).run(function(value) {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("Not a string")
|
||||
}
|
||||
return value
|
||||
})
|
||||
// errored1 is in an errored state
|
||||
```
|
||||
|
||||
```javascript
|
||||
var stream = m.prop(1)
|
||||
var errored2 = m.prop.combine(function(stream) {
|
||||
if (typeof stream() !== "string") {
|
||||
throw new Error("Not a string")
|
||||
}
|
||||
return stream()
|
||||
}, [stream])
|
||||
// errored2 is in an errored state
|
||||
```
|
||||
|
||||
When a stream is in a errored state, its value is set to `undefined` and its `error` method is set to the error value
|
||||
|
||||
```javascript
|
||||
var errored = m.prop.reject("Server is offline")
|
||||
|
||||
console.log(errored()) // logs undefined
|
||||
console.log(errored.error()) // logs "Server is offline"
|
||||
```
|
||||
|
||||
#### Ended state
|
||||
|
||||
A stream can stop affecting its dependent streams by calling `stream.end(true)`. This effectively removes the connection between a stream and its dependent streams.
|
||||
|
||||
```javascript
|
||||
var stream = m.prop()
|
||||
var doubled = stream.run(function(value) {return value * 2})
|
||||
|
||||
stream.end(true) // set to ended state
|
||||
|
||||
stream(5)
|
||||
|
||||
console.log(doubled())
|
||||
// logs undefined because `doubled` no longer depends on `stream`
|
||||
```
|
||||
|
||||
Ended streams still have state container semantics, i.e. you can still use them as getter-setters, even after they are ended.
|
||||
|
||||
```javascript
|
||||
var stream = m.prop(1)
|
||||
stream.end(true) // set to ended state
|
||||
|
||||
console.log(stream(1)) // logs 1
|
||||
|
||||
stream(2)
|
||||
console.log(stream()) // logs 2
|
||||
```
|
||||
|
||||
Ending a stream can be useful in cases where a stream has a limited lifetime (for example, reacting to `mousemove` events only while a DOM element is being dragged, but not after it's been dropped).
|
||||
|
||||
---
|
||||
|
||||
### Handling errors
|
||||
|
||||
When a stream is in a errored state, its value is set to `undefined`, and its `error` method returns the error value.
|
||||
|
||||
```javascript
|
||||
var erroredStream = m.prop.reject("Server is offline")
|
||||
|
||||
console.log(erroredStream()) // logs undefined
|
||||
console.log(erroredStream.error()) // logs "Server is offline"
|
||||
```
|
||||
|
||||
Errors can be set in various ways:
|
||||
|
||||
```javascript
|
||||
// via m.prop.reject
|
||||
var errored1 = m.prop.reject("Server is offline")
|
||||
console.log(errored1.error()) // logs "Server is offline"
|
||||
|
||||
// via `.error`
|
||||
var errored2 = m.prop("hello")
|
||||
errored2.error("Server is offline")
|
||||
console.log(errored2.error()) // logs "Server is offline"
|
||||
|
||||
// by throwing an error in a chain
|
||||
var errored3 = m.prop("hello").run(function() {
|
||||
throw "Server is offline"
|
||||
})
|
||||
console.log(errored3.error()) // logs "Server is offline"
|
||||
|
||||
var errored4 = m.prop.combine(function() {
|
||||
throw "Server is offline"
|
||||
}, [m.prop("hello")])
|
||||
console.log(errored4.error()) // logs "Server is offline"
|
||||
|
||||
// by returning an errored stream in a chain
|
||||
var errored5 = m.prop("hello").run(function() {
|
||||
return m.prop.reject("Server is offline")
|
||||
})
|
||||
console.log(errored5.error()) // logs "Server is offline"
|
||||
|
||||
var errored6 = m.prop.combine(function() {
|
||||
return m.prop.reject("Server is offline")
|
||||
}, [m.prop("hello")])
|
||||
console.log(errored6.error()) // logs "Server is offline"
|
||||
```
|
||||
|
||||
|
||||
Errors in stream chains propagate: if a stream is in an errored state, all of its dependent streams will have the same errored state, unless the error is handled via a `catch` method.
|
||||
|
||||
```javascript
|
||||
var dependentStream = erroredStream.run(function(value) {return value})
|
||||
console.log(dependentStream()) // logs undefined
|
||||
console.log(dependentStream.error()) // logs "Server is offline"
|
||||
|
||||
var recoveredStream = dependentStream.catch(function() {return "hello"})
|
||||
console.log(recoveredStream()) // logs "hello"
|
||||
console.log(recoveredStream.error()) // logs undefined
|
||||
```
|
||||
|
||||
Like in ES6 promises, the `catch` callback is only called if there is an error. If there isn't an error, it adopts the same value as its parent stream:
|
||||
|
||||
```javascript
|
||||
erroredStream("hi")
|
||||
|
||||
console.log(dependentStream()) // logs "hi"
|
||||
console.log(dependentStream.error()) // logs undefined
|
||||
|
||||
console.log(recoveredStream()) // logs "hi"
|
||||
console.log(recoveredStream.error()) // logs undefined
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Serializing streams
|
||||
|
||||
Streams implement a `.toJSON()` method. When a stream is passed as the argument to `JSON.stringify()`, the value of the stream is serialized.
|
||||
|
||||
```javascript
|
||||
var stream = m.prop(123)
|
||||
var serialized = JSON.stringify(stream)
|
||||
console.log(serialized) // logs 123
|
||||
```
|
||||
|
||||
Streams also implement a `valueOf` method that returns the value of the stream.
|
||||
|
||||
```javascript
|
||||
var stream = m.prop(123)
|
||||
console.log("test " + stream) // logs "test 123"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Streams do not trigger rendering
|
||||
|
||||
Unlike libraries like Knockout, Mithril streams do not trigger re-rendering of templates. Redrawing happens in response to event handlers defined in Mithril component views, route changes, or after [`m.request`](request.md) calls resolve.
|
||||
|
||||
If redrawing is desired in response to other asynchronous events (e.g. `setTimeout`/`setInterval`, websocket subscription, 3rd party library event handler, etc), you should manually call [`m.redraw()`](redraw.md)
|
||||
|
||||
---
|
||||
|
||||
### What is Fantasy Land
|
||||
|
||||
[Fantasy Land](https://github.com/fantasyland/fantasy-land) specifies interoperability of common algebraic structures. In plain english, that means that libraries that conform to Fantasy Land specs can be used to write generic functional style code that works regardless of how these libraries implement the constructs.
|
||||
|
||||
For example, say we want to create a generic function called `plusOne`. The naive implementation would look like this:
|
||||
|
||||
```javascript
|
||||
function plusOne(a) {
|
||||
return a + 1
|
||||
}
|
||||
```
|
||||
|
||||
The problem with this implementation is that it can only be used with a number. However it's possible that whatever logic produces a value for `a` could also produce an error state (wrapped in a Maybe or an Either from a library like [Sanctuary](https://github.com/sanctuary-js/sanctuary) or [Ramda-Fantasy](https://github.com/ramda/ramda-fantasy)), or it could be a Mithril stream, or a [flyd](https://github.com/paldepind/flyd) stream, etc. Ideally, we wouldn't want to write a similar version of the same function for every possible type that `a` could have and we wouldn't want to be writing wrapping/unwrapping/error handling code repeatedly.
|
||||
|
||||
This is where Fantasy Land can help. Let's rewrite that function in terms of a Fantasy Land algebra:
|
||||
|
||||
```javascript
|
||||
var fl = require("fantasy-land")
|
||||
|
||||
function plusOne(a) {
|
||||
return a[fl.map](function(value) {return value + 1})
|
||||
}
|
||||
```
|
||||
|
||||
Now this method works with any Fantasy Land compliant [Functor](https://github.com/fantasyland/fantasy-land#functor), such as [`R.Maybe`](https://github.com/ramda/ramda-fantasy/blob/master/docs/Maybe.md), [`S.Either`](https://github.com/sanctuary-js/sanctuary#either-type), `m.prop`, etc.
|
||||
|
||||
This example may seem convoluted, but it's a trade-off in complexity: the naive `plusOne` implementation makes sense if you have a simple system and only ever increment numbers, but the Fantasy Land implementation becomes more powerful if you have a large system with many wrapper abstractions and reused algorithms.
|
||||
|
||||
When deciding whether you should adopt Fantasy Land, you should consider your team's familiarity with functional programming, and be realistic regarding the level of discipline that your team can commit to maintaining code quality (vs the pressure of writing new features and meeting deadlines). Functional style programming heavily depends on compiling, curating and mastering a large set of small, precisely defined functions, and therefore it's not suitable for teams who do not have solid documentation practices, and/or lack experience in functional oriented languages.
|
||||
|
|
@ -71,7 +71,7 @@ var Data = {
|
|||
fetch: function() {
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/todos",
|
||||
url: "/api/v1/todos",
|
||||
})
|
||||
.then(function(items) {
|
||||
Data.todos.list = items
|
||||
|
|
@ -112,7 +112,7 @@ var Data = {
|
|||
fetch: function() {
|
||||
m.request({
|
||||
method: "GET",
|
||||
url: "/api/todos",
|
||||
url: "/api/v1/todos",
|
||||
})
|
||||
.then(function(items) {
|
||||
Data.todos.list = items
|
||||
|
|
|
|||
490
docs/stream.md
Normal file
490
docs/stream.md
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
# stream()
|
||||
|
||||
- [API](#api)
|
||||
- [Static members](#static-members)
|
||||
- [stream.combine](#streamcombine)
|
||||
- [stream.merge](#streammerge)
|
||||
- [stream.HALT](#streamhalt)
|
||||
- [stream["fantasy-land/of"]](#streamfantasy-landof)
|
||||
- [Instance members](#static-members)
|
||||
- [stream.map](#streammap)
|
||||
- [stream.end](#streamend)
|
||||
- [stream["fantasy-land/of"]](#streamfantasy-landof)
|
||||
- [stream["fantasy-land/map"]](#streamfantasy-landmap)
|
||||
- [stream["fantasy-land/ap"]](#streamfantasy-landap)
|
||||
- [Basic usage](#basic-usage)
|
||||
- [Streams as variables](#streams-as-variables)
|
||||
- [Bidirectional bindings](#bidirectional-bindings)
|
||||
- [Computed properties](#computed-properties)
|
||||
- [Chaining streams](#chaining-streams)
|
||||
- [Combining streams](#combining-streams)
|
||||
- [Stream states](#stream-states)
|
||||
- [Serializing streams](#serializing-streams)
|
||||
- [Streams do not trigger rendering](#streams-do-not-trigger-rendering)
|
||||
- [What is Fantasy Land](#what-is-fantasy-land)
|
||||
|
||||
---
|
||||
|
||||
### API
|
||||
|
||||
Creates a stream
|
||||
|
||||
`stream = stream(value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the stream is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
#### Static members
|
||||
|
||||
##### stream.combine
|
||||
|
||||
Creates a computed stream that reactively updates if any of its upstreams are updated. See [combining streams](#combining-streams)
|
||||
|
||||
`stream = stream.combine(combiner, streams)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | --------------------------- | -------- | ---
|
||||
`combiner` | `(Stream..., Array) -> any` | Yes | See [combiner](#combiner) argument
|
||||
`streams` | `Array<Stream>` | Yes | A list of streams to be combined
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
###### combiner
|
||||
|
||||
Specifies how the value of a computed stream is generated. See [combining streams](#combining-streams)
|
||||
|
||||
`any = combiner(streams..., changed)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`streams...` | splat of `Stream`s | No | Splat of zero or more streams that correspond to the streams passed as the second argument to [`stream.combine`](#stream-combine.md)
|
||||
`changed` | `Array<Stream>` | Yes | List of streams that were affected by an update
|
||||
**returns** | `any` | | Returns a computed value
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream.merge
|
||||
|
||||
Creates a stream whose value is the array of values from an array of streams
|
||||
|
||||
`stream = stream.merge(streams)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`streams` | `Array<Stream>` | Yes | A list of streams
|
||||
**returns** | `Stream` | | Returns a stream whose value is an array of input stream values
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream.HALT
|
||||
|
||||
A special value that can be returned to stream callbacks to halt execution of downstreams
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/of"]
|
||||
|
||||
This method is functionally identical to `stream`. It exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = stream["fantasy-land/of"](value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the stream is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
#### Instance members
|
||||
|
||||
##### stream.map
|
||||
|
||||
Creates a dependent stream whose value is set to the result of the callback function. This method is an alias of [stream["fantasy-land/map"]](#streamfantasy-landmap).
|
||||
|
||||
`dependentStream = stream().map(callback)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`callback` | `any -> any` | Yes | A callback whose return value becomes the value of the stream
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream.end
|
||||
|
||||
A co-dependent stream that unregisters dependent streams when set to true. See [ended state](#ended-state).
|
||||
|
||||
`endStream = stream().end`
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/of"]
|
||||
|
||||
This method is functionally identical to `stream`. It exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = stream()["fantasy-land/of"](value)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`value` | `any` | No | If this argument is present, the value of the stream is set to it
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/map"]
|
||||
|
||||
Creates a dependent stream whose value is set to the result of the callback function. See [chaining streams](#chaining-streams)
|
||||
|
||||
This method exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`dependentStream = stream()["fantasy-land/of"](callback)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
------------ | -------------------- | -------- | ---
|
||||
`callback` | `any -> any` | Yes | A callback whose return value becomes the value of the stream
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
[How to read signatures](signatures.md)
|
||||
|
||||
---
|
||||
|
||||
##### stream["fantasy-land/ap"]
|
||||
|
||||
The name of this method stands for `apply`. If a stream `a` has a function as its value, another stream `b` can use it as the argument to `b.ap(a)`. Calling `ap` will call the function with the value of stream `b` as its argument, and it will return another stream whose value is the result of the function call. This method exists to conform to [Fantasy Land's Applicative specification](https://github.com/fantasyland/fantasy-land). For more information, see the [What is Fantasy Land](#what-is-fantasy-land) section.
|
||||
|
||||
`stream = stream()["fantasy-land/ap"](apply)`
|
||||
|
||||
Argument | Type | Required | Description
|
||||
----------- | -------------------- | -------- | ---
|
||||
`apply` | `Stream` | Yes | A stream whose value is a function
|
||||
**returns** | `Stream` | | Returns a stream
|
||||
|
||||
---
|
||||
|
||||
### Basic usage
|
||||
|
||||
Streams are not part of the core Mithril distribution. To include them in a project, require its module:
|
||||
|
||||
```javascript
|
||||
var stream = require("mithril/stream")
|
||||
```
|
||||
|
||||
|
||||
#### Streams as variables
|
||||
|
||||
`stream()` returns a stream. At its most basic level, a stream works similar to a variable or a getter-setter property: it can hold state, which can be modified.
|
||||
|
||||
```javascript
|
||||
var username = stream("John")
|
||||
console.log(username()) // logs "John"
|
||||
|
||||
username("John Doe")
|
||||
console.log(username()) // logs "John Doe"
|
||||
```
|
||||
|
||||
The main difference is that a stream is a function, and therefore can be composed into higher order functions.
|
||||
|
||||
```javascript
|
||||
var users = stream()
|
||||
|
||||
// request users from a server using the fetch API
|
||||
fetch("/api/users")
|
||||
.then(function(response) {return response.json()})
|
||||
.then(users)
|
||||
```
|
||||
|
||||
In the example above, the `users` stream is populated with the response data when the request resolves.
|
||||
|
||||
#### Bidirectional bindings
|
||||
|
||||
Streams can also be populated from other higher order functions, such as [`m.withAttr`](withAttr.md)
|
||||
|
||||
```javascript
|
||||
// a stream
|
||||
var user = stream("")
|
||||
|
||||
// a bi-directional binding to the stream
|
||||
m("input", {
|
||||
oninput: m.withAttr("value", user),
|
||||
value: user()
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, when the user types in the input, the `user` stream is updated to the value of the input field.
|
||||
|
||||
#### Computed properties
|
||||
|
||||
Streams are useful for implementing computed properties:
|
||||
|
||||
```javascript
|
||||
var title = stream("")
|
||||
var slug = title.map(function(value) {
|
||||
return value.toLowerCase().replace(/\W/g, "-")
|
||||
})
|
||||
|
||||
title("Hello world")
|
||||
console.log(slug()) // logs "hello-world"
|
||||
```
|
||||
|
||||
In the example above, the value of `slug` is computed when `title` is updated, not when `slug` is read.
|
||||
|
||||
It's of course also possible to compute properties based on multiple streams:
|
||||
|
||||
```javascript
|
||||
var firstName = stream("John")
|
||||
var lastName = stream("Doe")
|
||||
var fullName = stream.merge([firstName, lastName]).map(function(values) {
|
||||
return values.join(" ")
|
||||
})
|
||||
|
||||
console.log(fullName()) // logs "John Doe"
|
||||
|
||||
firstName("Mary")
|
||||
|
||||
console.log(fullName()) // logs "Mary Doe"
|
||||
```
|
||||
|
||||
Computed properties in Mithril are updated atomically: streams that depend on multiple streams will never be called more than once per value update, no matter how complex the computed property's dependency graph is.
|
||||
|
||||
---
|
||||
|
||||
### Chaining streams
|
||||
|
||||
Streams can be chained using the `map` method. A chained stream is also known as a *dependent stream*.
|
||||
|
||||
```javascript
|
||||
// parent stream
|
||||
var value = stream(1)
|
||||
|
||||
// dependent stream
|
||||
var doubled = value.map(function(value) {
|
||||
return value * 2
|
||||
})
|
||||
|
||||
console.log(doubled()) // logs 2
|
||||
```
|
||||
|
||||
Dependent streams are *reactive*: their values are updated any time the value of their parent stream is updated. This happens regardless of whether the dependent stream was created before or after the value of the parent stream was set.
|
||||
|
||||
You can prevent dependent streams from being updated by returning the special value `stream.HALT`
|
||||
|
||||
```javascript
|
||||
var halted = stream(1).map(function(value) {
|
||||
return stream.HALT
|
||||
})
|
||||
|
||||
halted.map(function() {
|
||||
// never runs
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Combining streams
|
||||
|
||||
Streams can depend on more than one parent stream. These kinds of streams can be created via `stream.merge()`
|
||||
|
||||
```javascript
|
||||
var a = stream("hello")
|
||||
var b = stream("world")
|
||||
|
||||
var greeting = stream.merge([a, b]).map(function(values) {
|
||||
return values.join(" ")
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
```javascript
|
||||
var a = stream(5)
|
||||
var b = stream(7)
|
||||
|
||||
var added = stream.combine(function(a, b) {
|
||||
return a() + b()
|
||||
}, [a, b])
|
||||
|
||||
console.log(added()) // logs 12
|
||||
```
|
||||
|
||||
A stream can depend on any number of streams and it's guaranteed to update atomically. For example, if a stream A has two dependent streams B and C, and a fourth stream D is dependent on both B and C, the stream D will only update once if the value of A changes. This guarantees that the callback for stream D is never called with unstable values such as when B has a new value but C has the old value. Atomicity also bring the performance benefits of not recomputing downstreams unnecessarily.
|
||||
|
||||
You can prevent dependent streams from being updated by returning the special value `stream.HALT`
|
||||
|
||||
```javascript
|
||||
var halted = stream.combine(function(stream) {
|
||||
return stream.HALT
|
||||
}, [stream(1)])
|
||||
|
||||
halted.map(function() {
|
||||
// never runs
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Stream states
|
||||
|
||||
At any given time, a stream can be in one of three states: *pending*, *active*, and *ended*.
|
||||
|
||||
#### Pending state
|
||||
|
||||
Pending streams can be created by calling `stream()` with no parameters.
|
||||
|
||||
```javascript
|
||||
var pending = stream()
|
||||
```
|
||||
|
||||
If a stream is dependent on more than one stream, any of its parent streams is in a pending state, the dependent streams is also in a pending state, and does not update its value.
|
||||
|
||||
```javascript
|
||||
var a = stream(5)
|
||||
var b = stream() // pending stream
|
||||
|
||||
var added = stream.combine(function(a, b) {
|
||||
return a() + b()
|
||||
}, [a, b])
|
||||
|
||||
console.log(added()) // logs undefined
|
||||
```
|
||||
|
||||
In the example above, `added` is a pending stream, because its parent `b` is also pending.
|
||||
|
||||
This also applies to dependent streams created via `stream.map`:
|
||||
|
||||
```javascript
|
||||
var value = stream()
|
||||
var doubled = value.map(function(value) {return value * 2})
|
||||
|
||||
console.log(doubled()) // logs undefined because `doubled` is pending
|
||||
```
|
||||
|
||||
#### Active state
|
||||
|
||||
When a stream receives a value, it becomes active (unless the stream is ended).
|
||||
|
||||
```javascript
|
||||
var stream1 = stream("hello") // stream1 is active
|
||||
|
||||
var stream2 = stream() // stream2 starts off pending
|
||||
stream2("world") // then becomes active
|
||||
```
|
||||
|
||||
A dependent stream with multiple parents becomes active if all of its parents are active.
|
||||
|
||||
```javascript
|
||||
var a = stream("hello")
|
||||
var b = stream()
|
||||
|
||||
var greeting = stream.merge([a, b]).map(function(values) {
|
||||
return values.join(" ")
|
||||
})
|
||||
```
|
||||
|
||||
In the example above, the `a` stream is active, but `b` is pending. setting `b("world")` would cause `b` to become active, and therefore `greeting` would also become active, and be updated to have the value `"hello world"`
|
||||
|
||||
#### Ended state
|
||||
|
||||
A stream can stop affecting its dependent streams by calling `stream.end(true)`. This effectively removes the connection between a stream and its dependent streams.
|
||||
|
||||
```javascript
|
||||
var value = stream()
|
||||
var doubled = value.map(function(value) {return value * 2})
|
||||
|
||||
value.end(true) // set to ended state
|
||||
|
||||
value(5)
|
||||
|
||||
console.log(doubled())
|
||||
// logs undefined because `doubled` no longer depends on `value`
|
||||
```
|
||||
|
||||
Ended streams still have state container semantics, i.e. you can still use them as getter-setters, even after they are ended.
|
||||
|
||||
```javascript
|
||||
var value = stream(1)
|
||||
value.end(true) // set to ended state
|
||||
|
||||
console.log(value(1)) // logs 1
|
||||
|
||||
value(2)
|
||||
console.log(value()) // logs 2
|
||||
```
|
||||
|
||||
Ending a stream can be useful in cases where a stream has a limited lifetime (for example, reacting to `mousemove` events only while a DOM element is being dragged, but not after it's been dropped).
|
||||
|
||||
---
|
||||
|
||||
### Serializing streams
|
||||
|
||||
Streams implement a `.toJSON()` method. When a stream is passed as the argument to `JSON.stringify()`, the value of the stream is serialized.
|
||||
|
||||
```javascript
|
||||
var value = stream(123)
|
||||
var serialized = JSON.stringify(value)
|
||||
console.log(serialized) // logs 123
|
||||
```
|
||||
|
||||
Streams also implement a `valueOf` method that returns the value of the stream.
|
||||
|
||||
```javascript
|
||||
var value = stream(123)
|
||||
console.log("test " + value) // logs "test 123"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Streams do not trigger rendering
|
||||
|
||||
Unlike libraries like Knockout, Mithril streams do not trigger re-rendering of templates. Redrawing happens in response to event handlers defined in Mithril component views, route changes, or after [`m.request`](request.md) calls resolve.
|
||||
|
||||
If redrawing is desired in response to other asynchronous events (e.g. `setTimeout`/`setInterval`, websocket subscription, 3rd party library event handler, etc), you should manually call [`m.redraw()`](redraw.md)
|
||||
|
||||
---
|
||||
|
||||
### What is Fantasy Land
|
||||
|
||||
[Fantasy Land](https://github.com/fantasyland/fantasy-land) specifies interoperability of common algebraic structures. In plain english, that means that libraries that conform to Fantasy Land specs can be used to write generic functional style code that works regardless of how these libraries implement the constructs.
|
||||
|
||||
For example, say we want to create a generic function called `plusOne`. The naive implementation would look like this:
|
||||
|
||||
```javascript
|
||||
function plusOne(a) {
|
||||
return a + 1
|
||||
}
|
||||
```
|
||||
|
||||
The problem with this implementation is that it can only be used with a number. However it's possible that whatever logic produces a value for `a` could also produce an error state (wrapped in a Maybe or an Either from a library like [Sanctuary](https://github.com/sanctuary-js/sanctuary) or [Ramda-Fantasy](https://github.com/ramda/ramda-fantasy)), or it could be a Mithril stream, or a [flyd](https://github.com/paldepind/flyd) stream, etc. Ideally, we wouldn't want to write a similar version of the same function for every possible type that `a` could have and we wouldn't want to be writing wrapping/unwrapping/error handling code repeatedly.
|
||||
|
||||
This is where Fantasy Land can help. Let's rewrite that function in terms of a Fantasy Land algebra:
|
||||
|
||||
```javascript
|
||||
var fl = require("fantasy-land")
|
||||
|
||||
function plusOne(a) {
|
||||
return a[fl.map](function(value) {return value + 1})
|
||||
}
|
||||
```
|
||||
|
||||
Now this method works with any Fantasy Land compliant [Functor](https://github.com/fantasyland/fantasy-land#functor), such as [`R.Maybe`](https://github.com/ramda/ramda-fantasy/blob/master/docs/Maybe.md), [`S.Either`](https://github.com/sanctuary-js/sanctuary#either-type), `stream`, etc.
|
||||
|
||||
This example may seem convoluted, but it's a trade-off in complexity: the naive `plusOne` implementation makes sense if you have a simple system and only ever increment numbers, but the Fantasy Land implementation becomes more powerful if you have a large system with many wrapper abstractions and reused algorithms.
|
||||
|
||||
When deciding whether you should adopt Fantasy Land, you should consider your team's familiarity with functional programming, and be realistic regarding the level of discipline that your team can commit to maintaining code quality (vs the pressure of writing new features and meeting deadlines). Functional style programming heavily depends on compiling, curating and mastering a large set of small, precisely defined functions, and therefore it's not suitable for teams who do not have solid documentation practices, and/or lack experience in functional oriented languages.
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
Creates an event handler. The event handler takes the value of a DOM element's property and calls a function with it as the argument.
|
||||
|
||||
This helper function is typically used in conjunction with [`m.prop()`](prop.md) to implement data binding. It is provided to help decouple the browser's event model from application code.
|
||||
This helper function is provided to help decouple the browser's event model from application code.
|
||||
|
||||
`m.withAttr(attrName, callback, thisArg?)`
|
||||
|
||||
|
|
@ -35,16 +35,21 @@ document.body.onclick = m.withAttr("title", function(value) {
|
|||
})
|
||||
```
|
||||
|
||||
Typically, `m.withAttr()` can be used in Mithril component views to implement two-way binding:
|
||||
Typically, `m.withAttr()` can be used in Mithril component views to avoid polluting the data layer with DOM event model concerns:
|
||||
|
||||
```javascript
|
||||
var title = m.prop()
|
||||
var Data = {
|
||||
email: "",
|
||||
setEmail: function(email) {
|
||||
Data.email = email.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
var MyComponent = {
|
||||
view: function() {
|
||||
return m("input", {
|
||||
oninput: m.withAttr("value", title),
|
||||
value: title()
|
||||
oninput: m.withAttr("value", Data.setEmail),
|
||||
value: Data.email
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -59,12 +64,15 @@ m.mount(document.body, MyComponent)
|
|||
The `m.withAttr()` helper reads the value of the element to which the event handler is bound, which is not necessarily the same as the element where the event originated.
|
||||
|
||||
```javascript
|
||||
var url = m.prop()
|
||||
var Data = {
|
||||
url: "",
|
||||
setURL: function(url) {Data.url = url}
|
||||
}
|
||||
|
||||
var MyComponent = {
|
||||
view: function() {
|
||||
return m("a[href='/foo']", {onclick: m.withAttr("href", url)}, [
|
||||
m("span", url())
|
||||
return m("a[href='/foo']", {onclick: m.withAttr("href", Data.setURL)}, [
|
||||
m("span", Data.url)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -84,15 +92,21 @@ The first argument of `m.withAttr()` can be either an attribute or a property.
|
|||
|
||||
```javascript
|
||||
// reads from `select.selectedIndex` property
|
||||
var index = m.prop(0)
|
||||
m("select", {onclick: m.withAttr("selectedIndex", index)})
|
||||
var Data = {
|
||||
index: 0,
|
||||
setIndex: function(index) {Data.index = index}
|
||||
}
|
||||
m("select", {onclick: m.withAttr("selectedIndex", Data.setIndex)})
|
||||
```
|
||||
|
||||
If a value can be both an attribute *and* a property, the property value is used.
|
||||
|
||||
```javascript
|
||||
// value is a boolean, because the `input.checked` property is boolean
|
||||
var value = m.prop(false)
|
||||
m("input", {onclick: m.withAttr("checked", value)})
|
||||
var Data = {
|
||||
selected: false,
|
||||
setSelected: function(selected) {Data.selected = selected}
|
||||
}
|
||||
m("input[type=checkbox]", {onclick: m.withAttr("checked", Data.setSelected)})
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
module.exports = require("./util/stream")(console.log.bind(console))
|
||||
module.exports = require("./stream/stream")
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
//! adapted for mithril from flyd https://github.com/paldepind/flyd
|
||||
"use strict"
|
||||
|
||||
var combine = require("../stream").combine
|
||||
|
||||
module.exports = function (tuples, seed) {
|
||||
var streams = tuples.map(function (tuple) {
|
||||
var stream = tuple[0]
|
||||
if (stream._state.state === 0) stream(undefined)
|
||||
return stream
|
||||
})
|
||||
|
||||
var newStream = combine(function () {
|
||||
var changed = arguments[arguments.length - 1]
|
||||
|
||||
streams.forEach(function (stream, idx) {
|
||||
if (changed.indexOf(stream) > -1) {
|
||||
seed = tuples[idx][1](seed, stream._state.value)
|
||||
}
|
||||
})
|
||||
|
||||
return seed
|
||||
}, streams)
|
||||
|
||||
return newStream
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
//! adapted for mithril from flyd https://github.com/paldepind/flyd
|
||||
"use strict"
|
||||
|
||||
var combine = require("../stream").combine
|
||||
var combine = require("./stream").combine
|
||||
|
||||
module.exports = function (reducer, seed, stream) {
|
||||
var newStream = combine(function (s) {
|
||||
|
|
|
|||
25
stream/scanMerge.js
Normal file
25
stream/scanMerge.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"use strict"
|
||||
|
||||
var combine = require("./stream").combine
|
||||
|
||||
module.exports = function(tuples, seed) {
|
||||
var streams = tuples.map(function(tuple) {
|
||||
var stream = tuple[0]
|
||||
if (stream._state.state === 0) stream(undefined)
|
||||
return stream
|
||||
})
|
||||
|
||||
var newStream = combine(function() {
|
||||
var changed = arguments[arguments.length - 1]
|
||||
|
||||
streams.forEach(function(stream, idx) {
|
||||
if (changed.indexOf(stream) > -1) {
|
||||
seed = tuples[idx][1](seed, stream._state.value)
|
||||
}
|
||||
})
|
||||
|
||||
return seed
|
||||
}, streams)
|
||||
|
||||
return newStream
|
||||
}
|
||||
115
stream/stream.js
Normal file
115
stream/stream.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"use strict"
|
||||
|
||||
var guid = 0, noop = function() {}, HALT = {}
|
||||
function createStream() {
|
||||
function stream() {
|
||||
if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, arguments[0])
|
||||
return stream._state.value
|
||||
}
|
||||
initStream(stream)
|
||||
|
||||
if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, arguments[0])
|
||||
|
||||
return stream
|
||||
}
|
||||
function initStream(stream) {
|
||||
stream.constructor = createStream
|
||||
stream._state = {id: guid++, value: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], endStream: undefined}
|
||||
stream.map = stream["fantasy-land/map"] = map, stream["fantasy-land/ap"] = ap, stream["fantasy-land/of"] = createStream
|
||||
stream.valueOf = valueOf, stream.toJSON = toJSON, stream.toString = valueOf
|
||||
|
||||
Object.defineProperties(stream, {
|
||||
end: {get: function() {
|
||||
if (!stream._state.endStream) {
|
||||
var endStream = createStream()
|
||||
endStream.map(function(value) {
|
||||
if (value === true) unregisterStream(stream), unregisterStream(endStream)
|
||||
return value
|
||||
})
|
||||
stream._state.endStream = endStream
|
||||
}
|
||||
return stream._state.endStream
|
||||
}}
|
||||
})
|
||||
}
|
||||
function updateStream(stream, value) {
|
||||
updateState(stream, value)
|
||||
for (var id in stream._state.deps) updateDependency(stream._state.deps[id], false)
|
||||
finalize(stream)
|
||||
}
|
||||
function updateState(stream, value) {
|
||||
stream._state.value = value
|
||||
stream._state.changed = true
|
||||
if (stream._state.state !== 2) stream._state.state = 1
|
||||
}
|
||||
function updateDependency(stream, mustSync) {
|
||||
var state = stream._state, parents = state.parents
|
||||
if (parents.length > 0 && parents.filter(active).length === parents.length && (mustSync || parents.filter(changed).length > 0)) {
|
||||
var value = stream._state.derive()
|
||||
if (value === HALT) return false
|
||||
updateState(stream, value)
|
||||
}
|
||||
}
|
||||
function finalize(stream) {
|
||||
stream._state.changed = false
|
||||
for (var id in stream._state.deps) stream._state.deps[id]._state.changed = false
|
||||
}
|
||||
|
||||
function combine(fn, streams) {
|
||||
if (streams.length > streams.filter(valid).length) throw new Error("Ensure that each item passed to m.prop.combine/m.prop.merge is a stream")
|
||||
return initDependency(createStream(), streams, function() {
|
||||
return fn.apply(this, streams.concat([streams.filter(changed)]))
|
||||
})
|
||||
}
|
||||
|
||||
function initDependency(dep, streams, derive) {
|
||||
var state = dep._state
|
||||
state.derive = derive
|
||||
state.parents = streams.filter(notEnded)
|
||||
|
||||
registerDependency(dep, state.parents)
|
||||
updateDependency(dep, true)
|
||||
|
||||
return dep
|
||||
}
|
||||
function registerDependency(stream, parents) {
|
||||
for (var i = 0; i < parents.length; i++) {
|
||||
parents[i]._state.deps[stream._state.id] = stream
|
||||
registerDependency(stream, parents[i]._state.parents)
|
||||
}
|
||||
}
|
||||
function unregisterStream(stream) {
|
||||
for (var i = 0; i < stream._state.parents.length; i++) {
|
||||
var parent = stream._state.parents[i]
|
||||
delete parent._state.deps[stream._state.id]
|
||||
}
|
||||
for (var id in stream._state.deps) {
|
||||
var dependent = stream._state.deps[id]
|
||||
var index = dependent._state.parents.indexOf(stream)
|
||||
if (index > -1) dependent._state.parents.splice(index, 1)
|
||||
}
|
||||
stream._state.state = 2 //ended
|
||||
stream._state.deps = {}
|
||||
}
|
||||
|
||||
function map(fn) {return combine(function(stream) {return fn(stream())}, [this])}
|
||||
function ap(stream) {return combine(function(s1, s2) {return s1()(s2())}, [stream, this])}
|
||||
function valueOf() {return this._state.value}
|
||||
function toJSON() {return this._state.value != null && typeof this._state.value.toJSON === "function" ? this._state.value.toJSON() : this._state.value}
|
||||
|
||||
function valid(stream) {return stream._state }
|
||||
function active(stream) {return stream._state.state === 1}
|
||||
function changed(stream) {return stream._state.changed}
|
||||
function notEnded(stream) {return stream._state.state !== 2}
|
||||
|
||||
function merge(streams) {
|
||||
return combine(function() {
|
||||
return streams.map(function(s) {return s()})
|
||||
}, streams)
|
||||
}
|
||||
createStream["fantasy-land/of"] = createStream
|
||||
createStream.merge = merge
|
||||
createStream.combine = combine
|
||||
createStream.HALT = HALT
|
||||
|
||||
module.exports = createStream
|
||||
|
|
@ -6,11 +6,13 @@
|
|||
<body>
|
||||
<script src="../../module/module.js"></script>
|
||||
<script src="../../ospec/ospec.js"></script>
|
||||
<script src="../../util/stream.js"></script>
|
||||
<script src="../../stream.js"></script>
|
||||
<script src="../../test-utils/callAsync.js"></script>
|
||||
<script src="../stream.js"></script>
|
||||
<script src="../scan.js"></script>
|
||||
<script src="../scan-merge.js"></script>
|
||||
<script src="../scanMerge.js"></script>
|
||||
<script src="test-stream.js"></script>
|
||||
<script src="test-scanMerge.js"></script>
|
||||
<script src="test-scanMerge.js"></script>
|
||||
|
||||
<script>require("../../ospec/ospec").run()</script>
|
||||
</body>
|
||||
|
|
|
|||
78
stream/tests/test-scan.js
Normal file
78
stream/tests/test-scan.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"use strict"
|
||||
|
||||
var o = require("../../ospec/ospec")
|
||||
var stream = require("../stream")
|
||||
var scan = require("../scan")
|
||||
|
||||
o.spec("scan", function() {
|
||||
o("defaults to seed", function() {
|
||||
var parent = stream()
|
||||
var child = scan(function(out, p) {
|
||||
return out - p
|
||||
}, 123, parent)
|
||||
o(child()).equals(123)
|
||||
})
|
||||
|
||||
o("accumulates values as expected", function() {
|
||||
var parent = stream()
|
||||
var child = scan(function(arr, p) {
|
||||
return arr.concat(p)
|
||||
}, [], parent)
|
||||
|
||||
parent(7)
|
||||
parent("11")
|
||||
parent(undefined)
|
||||
parent({ a: 1 })
|
||||
var result = child()
|
||||
|
||||
// deepEquals fails on arrays?
|
||||
o(result[0]).equals(7)
|
||||
o(result[1]).equals("11")
|
||||
o(result[2]).equals(undefined)
|
||||
o(result[3]).deepEquals({a: 1})
|
||||
})
|
||||
})
|
||||
|
||||
o.spec("scanMerge", function() {
|
||||
var scanMerge
|
||||
|
||||
o.beforeEach(function() {
|
||||
scanMerge = require("../scan-merge")
|
||||
})
|
||||
|
||||
o("defaults to seed", function() {
|
||||
var parent1 = stream()
|
||||
var parent2 = stream()
|
||||
|
||||
var child = scanMerge([
|
||||
[parent1, function(out, p1) {
|
||||
return out + p1
|
||||
}],
|
||||
[parent2, function(out, p2) {
|
||||
return out + p2
|
||||
}]
|
||||
], -10)
|
||||
|
||||
o(child()).equals(-10)
|
||||
})
|
||||
|
||||
o("accumulates as expected", function() {
|
||||
var parent1 = stream()
|
||||
var parent2 = stream()
|
||||
|
||||
var child = scanMerge([
|
||||
[parent1, function(out, p1) {
|
||||
return out + p1
|
||||
}],
|
||||
[parent2, function(out, p2) {
|
||||
return out + p2 + p2
|
||||
}]
|
||||
], "a")
|
||||
|
||||
parent1("b")
|
||||
parent2("c")
|
||||
parent1("b")
|
||||
|
||||
o(child()).equals("abccb")
|
||||
})
|
||||
})
|
||||
43
stream/tests/test-scanMerge.js
Normal file
43
stream/tests/test-scanMerge.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"use strict"
|
||||
|
||||
var o = require("../../ospec/ospec")
|
||||
var stream = require("../stream")
|
||||
var scanMerge = require("../scanMerge")
|
||||
|
||||
o.spec("scanMerge", function() {
|
||||
o("defaults to seed", function() {
|
||||
var parent1 = stream()
|
||||
var parent2 = stream()
|
||||
|
||||
var child = scanMerge([
|
||||
[parent1, function(out, p1) {
|
||||
return out + p1
|
||||
}],
|
||||
[parent2, function(out, p2) {
|
||||
return out + p2
|
||||
}]
|
||||
], -10)
|
||||
|
||||
o(child()).equals(-10)
|
||||
})
|
||||
|
||||
o("accumulates as expected", function() {
|
||||
var parent1 = stream()
|
||||
var parent2 = stream()
|
||||
|
||||
var child = scanMerge([
|
||||
[parent1, function(out, p1) {
|
||||
return out + p1
|
||||
}],
|
||||
[parent2, function(out, p2) {
|
||||
return out + p2 + p2
|
||||
}]
|
||||
], "a")
|
||||
|
||||
parent1("b")
|
||||
parent2("c")
|
||||
parent1("b")
|
||||
|
||||
o(child()).equals("abccb")
|
||||
})
|
||||
})
|
||||
|
|
@ -1,90 +1,452 @@
|
|||
'use strict'
|
||||
"use strict"
|
||||
|
||||
var o = require('../../ospec/ospec')
|
||||
var o = require("../../ospec/ospec")
|
||||
var callAsync = require("../../test-utils/callAsync")
|
||||
var Stream = require("../stream")
|
||||
|
||||
o.spec('stream', function () {
|
||||
var prop
|
||||
o.spec("stream", function() {
|
||||
o.spec("stream", function() {
|
||||
o("works as getter/setter", function() {
|
||||
var stream = Stream(1)
|
||||
var initialValue = stream()
|
||||
stream(2)
|
||||
var newValue = stream()
|
||||
|
||||
o.beforeEach(function () {
|
||||
prop = require('../../stream')
|
||||
o(initialValue).equals(1)
|
||||
o(newValue).equals(2)
|
||||
})
|
||||
o("has undefined value by default", function() {
|
||||
var stream = Stream()
|
||||
|
||||
o.spec('scan', function () {
|
||||
var scan
|
||||
|
||||
o.beforeEach(function () {
|
||||
scan = require('../scan')
|
||||
o(stream()).equals(undefined)
|
||||
})
|
||||
o("can update to undefined", function() {
|
||||
var stream = Stream(1)
|
||||
stream(undefined)
|
||||
|
||||
o('defaults to seed', function () {
|
||||
var parent = prop()
|
||||
var child = scan(function (out, p) {
|
||||
return out - p
|
||||
}, 123, parent)
|
||||
o(child()).equals(123)
|
||||
o(stream()).equals(undefined)
|
||||
})
|
||||
o("can be stream of streams", function() {
|
||||
var stream = Stream(Stream(1))
|
||||
|
||||
o('accumulates values as expected', function () {
|
||||
var parent = prop()
|
||||
var child = scan(function (arr, p) {
|
||||
return arr.concat(p)
|
||||
}, [], parent)
|
||||
|
||||
parent(7)
|
||||
parent('11')
|
||||
parent(undefined)
|
||||
parent({ a: 1 })
|
||||
var result = child()
|
||||
|
||||
// deepEquals fails on arrays?
|
||||
o(result[0]).equals(7)
|
||||
o(result[1]).equals('11')
|
||||
o(result[2]).equals(undefined)
|
||||
o(result[3]).deepEquals({ a: 1 })
|
||||
o(stream()()).equals(1)
|
||||
})
|
||||
})
|
||||
o.spec("combine", function() {
|
||||
o("transforms value", function() {
|
||||
var stream = Stream()
|
||||
var doubled = Stream.combine(function(s) {return s() * 2}, [stream])
|
||||
|
||||
o.spec('scanMerge', function () {
|
||||
var scanMerge
|
||||
stream(2)
|
||||
|
||||
o.beforeEach(function () {
|
||||
scanMerge = require('../scan-merge')
|
||||
o(doubled()).equals(4)
|
||||
})
|
||||
o("transforms default value", function() {
|
||||
var stream = Stream(2)
|
||||
var doubled = Stream.combine(function(s) {return s() * 2}, [stream])
|
||||
|
||||
o(doubled()).equals(4)
|
||||
})
|
||||
o("transforms multiple values", function() {
|
||||
var s1 = Stream()
|
||||
var s2 = Stream()
|
||||
var added = Stream.combine(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.combine(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.combine(function(s1, s2) {return s1() + s2()}, [s1, s2])
|
||||
|
||||
s2(3)
|
||||
|
||||
o(added()).equals(5)
|
||||
})
|
||||
o("combines atomically", function() {
|
||||
var count = 0
|
||||
var a = Stream()
|
||||
var b = Stream.combine(function(a) {return a() * 2}, [a])
|
||||
var c = Stream.combine(function(a) {return a() * a()}, [a])
|
||||
var d = Stream.combine(function(b, c) {
|
||||
count++
|
||||
return b() + c()
|
||||
}, [b, c])
|
||||
|
||||
a(3)
|
||||
|
||||
o(d()).equals(15)
|
||||
o(count).equals(1)
|
||||
})
|
||||
o("combines default value atomically", function() {
|
||||
var count = 0
|
||||
var a = Stream(3)
|
||||
var b = Stream.combine(function(a) {return a() * 2}, [a])
|
||||
var c = Stream.combine(function(a) {return a() * a()}, [a])
|
||||
var d = Stream.combine(function(b, c) {
|
||||
count++
|
||||
return b() + c()
|
||||
}, [b, c])
|
||||
|
||||
o(d()).equals(15)
|
||||
o(count).equals(1)
|
||||
})
|
||||
o("combine lists only changed upstreams in last arg", function() {
|
||||
var streams = []
|
||||
var a = Stream()
|
||||
var b = Stream()
|
||||
var c = Stream.combine(function(a, b, changed) {
|
||||
streams = changed
|
||||
}, [a, b])
|
||||
|
||||
a(3)
|
||||
b(5)
|
||||
|
||||
o(streams.length).equals(1)
|
||||
o(streams[0]).equals(b)
|
||||
})
|
||||
o("combine lists only changed upstreams in last arg with default value", function() {
|
||||
var streams = []
|
||||
var a = Stream(3)
|
||||
var b = Stream(5)
|
||||
var c = Stream.combine(function(a, b, changed) {
|
||||
streams = changed
|
||||
}, [a, b])
|
||||
|
||||
a(7)
|
||||
|
||||
o(streams.length).equals(1)
|
||||
o(streams[0]).equals(a)
|
||||
})
|
||||
o("combine can return undefined", function() {
|
||||
var a = Stream(1)
|
||||
var b = Stream.combine(function(a) {
|
||||
return undefined
|
||||
}, [a])
|
||||
|
||||
o(b()).equals(undefined)
|
||||
})
|
||||
o("combine can return stream", function() {
|
||||
var a = Stream(1)
|
||||
var b = Stream.combine(function(a) {
|
||||
return Stream(2)
|
||||
}, [a])
|
||||
|
||||
o(b()()).equals(2)
|
||||
})
|
||||
o("combine can return pending stream", function() {
|
||||
var a = Stream(1)
|
||||
var b = Stream.combine(function(a) {
|
||||
return Stream()
|
||||
}, [a])
|
||||
|
||||
o(b()()).equals(undefined)
|
||||
})
|
||||
o("combine can halt", function() {
|
||||
var count = 0
|
||||
var a = Stream(1)
|
||||
var b = Stream.combine(function(a) {
|
||||
return Stream.HALT
|
||||
}, [a])
|
||||
["fantasy-land/map"](function() {
|
||||
count++
|
||||
return 1
|
||||
})
|
||||
|
||||
o('defaults to seed', function () {
|
||||
var parent1 = prop()
|
||||
var parent2 = prop()
|
||||
o(b()).equals(undefined)
|
||||
})
|
||||
o("combine will throw with a helpful error if given non-stream values", function () {
|
||||
var spy = o.spy()
|
||||
var a = Stream(1)
|
||||
var thrown = null;
|
||||
try {
|
||||
var b = Stream.combine(spy, [a, ''])
|
||||
} catch (e) {
|
||||
thrown = e
|
||||
}
|
||||
|
||||
var child = scanMerge([
|
||||
[parent1, function (out, p1) {
|
||||
return out + p1
|
||||
}],
|
||||
[parent2, function (out, p2) {
|
||||
return out + p2
|
||||
}]
|
||||
], -10)
|
||||
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([
|
||||
Stream(10),
|
||||
Stream("20"),
|
||||
Stream({value: 30}),
|
||||
])
|
||||
|
||||
o(child()).equals(-10)
|
||||
o(all()).deepEquals([10, "20", {value: 30}])
|
||||
})
|
||||
o("remains pending until all streams are active", function() {
|
||||
var straggler = Stream()
|
||||
|
||||
var all = Stream.merge([
|
||||
Stream(10),
|
||||
Stream("20"),
|
||||
straggler,
|
||||
])
|
||||
|
||||
o(all()).equals(undefined)
|
||||
|
||||
straggler(30)
|
||||
o(all()).deepEquals([10, "20", 30])
|
||||
})
|
||||
o("calls run callback after all parents are active", function() {
|
||||
var value = 0
|
||||
var id = function(value) {return value}
|
||||
var a = Stream()
|
||||
var b = Stream()
|
||||
|
||||
var all = Stream.merge([a.map(id), b.map(id)]).map(function(data) {
|
||||
value = data[0] + data[1]
|
||||
})
|
||||
|
||||
o('accumulates as expected', function () {
|
||||
var parent1 = prop()
|
||||
var parent2 = prop()
|
||||
a(1)
|
||||
b(2)
|
||||
o(value).equals(3)
|
||||
|
||||
var child = scanMerge([
|
||||
[parent1, function (out, p1) {
|
||||
return out + p1
|
||||
}],
|
||||
[parent2, function (out, p2) {
|
||||
return out + p2 + p2
|
||||
}]
|
||||
], 'a')
|
||||
a(3)
|
||||
b(4)
|
||||
o(value).equals(7)
|
||||
})
|
||||
})
|
||||
o.spec("end", function() {
|
||||
o("end stream works", function() {
|
||||
var stream = Stream()
|
||||
var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream])
|
||||
|
||||
parent1('b')
|
||||
parent2('c')
|
||||
parent1('b')
|
||||
stream.end(true)
|
||||
|
||||
o(child()).equals('abccb')
|
||||
stream(3)
|
||||
|
||||
o(doubled()).equals(undefined)
|
||||
})
|
||||
o("end stream works with default value", function() {
|
||||
var stream = Stream(2)
|
||||
var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream])
|
||||
|
||||
stream.end(true)
|
||||
|
||||
stream(3)
|
||||
|
||||
o(doubled()).equals(4)
|
||||
})
|
||||
o("cannot add downstream to ended stream", function() {
|
||||
var stream = Stream(2)
|
||||
stream.end(true)
|
||||
|
||||
var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream])
|
||||
stream(3)
|
||||
|
||||
o(doubled()).equals(undefined)
|
||||
})
|
||||
o("upstream does not affect ended stream", function() {
|
||||
var stream = Stream(2)
|
||||
var doubled = Stream.combine(function(stream) {return stream() * 2}, [stream])
|
||||
|
||||
doubled.end(true)
|
||||
|
||||
stream(4)
|
||||
|
||||
o(doubled()).equals(4)
|
||||
})
|
||||
})
|
||||
o.spec("valueOf", function() {
|
||||
o("works", function() {
|
||||
o(Stream(1).valueOf()).equals(1)
|
||||
o(Stream("a").valueOf()).equals("a")
|
||||
o(Stream(true).valueOf()).equals(true)
|
||||
o(Stream(null).valueOf()).equals(null)
|
||||
o(Stream(undefined).valueOf()).equals(undefined)
|
||||
o(Stream({a: 1}).valueOf()).deepEquals({a: 1})
|
||||
o(Stream([1, 2, 3]).valueOf()).deepEquals([1, 2, 3])
|
||||
o(Stream().valueOf()).equals(undefined)
|
||||
})
|
||||
o("allows implicit value access in mathematical operations", function() {
|
||||
o(Stream(1) + Stream(1)).equals(2)
|
||||
})
|
||||
})
|
||||
o.spec("toString", function() {
|
||||
o("aliases valueOf", function() {
|
||||
var stream = Stream(1)
|
||||
|
||||
o(stream.toString).equals(stream.valueOf)
|
||||
})
|
||||
o("allows implicit value access in string operations", function() {
|
||||
o(Stream("a") + Stream("b")).equals("ab")
|
||||
})
|
||||
})
|
||||
o.spec("toJSON", function() {
|
||||
o("works", function() {
|
||||
o(Stream(1).toJSON()).equals(1)
|
||||
o(Stream("a").toJSON()).equals("a")
|
||||
o(Stream(true).toJSON()).equals(true)
|
||||
o(Stream(null).toJSON()).equals(null)
|
||||
o(Stream(undefined).toJSON()).equals(undefined)
|
||||
o(Stream({a: 1}).toJSON()).deepEquals({a: 1})
|
||||
o(Stream([1, 2, 3]).toJSON()).deepEquals([1, 2, 3])
|
||||
o(Stream().toJSON()).equals(undefined)
|
||||
o(Stream(new Date(0)).toJSON()).equals(new Date(0).toJSON())
|
||||
})
|
||||
o("works w/ JSON.stringify", function() {
|
||||
o(JSON.stringify(Stream(1))).equals(JSON.stringify(1))
|
||||
o(JSON.stringify(Stream("a"))).equals(JSON.stringify("a"))
|
||||
o(JSON.stringify(Stream(true))).equals(JSON.stringify(true))
|
||||
o(JSON.stringify(Stream(null))).equals(JSON.stringify(null))
|
||||
o(JSON.stringify(Stream(undefined))).equals(JSON.stringify(undefined))
|
||||
o(JSON.stringify(Stream({a: 1}))).deepEquals(JSON.stringify({a: 1}))
|
||||
o(JSON.stringify(Stream([1, 2, 3]))).deepEquals(JSON.stringify([1, 2, 3]))
|
||||
o(JSON.stringify(Stream())).equals(JSON.stringify(undefined))
|
||||
o(JSON.stringify(Stream(new Date(0)))).equals(JSON.stringify(new Date(0)))
|
||||
})
|
||||
})
|
||||
o.spec("map", function() {
|
||||
o("works", function() {
|
||||
var stream = Stream()
|
||||
var doubled = stream["fantasy-land/map"](function(value) {return value * 2})
|
||||
|
||||
stream(3)
|
||||
|
||||
o(doubled()).equals(6)
|
||||
})
|
||||
o("works with default value", function() {
|
||||
var stream = Stream(3)
|
||||
var doubled = stream["fantasy-land/map"](function(value) {return value * 2})
|
||||
|
||||
o(doubled()).equals(6)
|
||||
})
|
||||
o("works with undefined value", function() {
|
||||
var stream = Stream()
|
||||
var mapped = stream["fantasy-land/map"](function(value) {return String(value)})
|
||||
|
||||
stream(undefined)
|
||||
|
||||
o(mapped()).equals("undefined")
|
||||
})
|
||||
o("works with default undefined value", function() {
|
||||
var stream = Stream(undefined)
|
||||
var mapped = stream["fantasy-land/map"](function(value) {return String(value)})
|
||||
|
||||
o(mapped()).equals("undefined")
|
||||
})
|
||||
o("works with pending stream", function() {
|
||||
var stream = Stream(undefined)
|
||||
var mapped = stream["fantasy-land/map"](function(value) {return Stream()})
|
||||
|
||||
o(mapped()()).equals(undefined)
|
||||
})
|
||||
o("has alias", function() {
|
||||
var stream = Stream(undefined)
|
||||
|
||||
o(stream["fantasy-land/map"]).equals(stream.map)
|
||||
})
|
||||
})
|
||||
o.spec("ap", function() {
|
||||
o("works", function() {
|
||||
var apply = Stream(function(value) {return value * 2})
|
||||
var stream = Stream(3)
|
||||
var applied = stream["fantasy-land/ap"](apply)
|
||||
|
||||
o(applied()).equals(6)
|
||||
|
||||
apply(function(value) {return value / 3})
|
||||
|
||||
o(applied()).equals(1)
|
||||
|
||||
stream(9)
|
||||
|
||||
o(applied()).equals(3)
|
||||
})
|
||||
o("works with undefined value", function() {
|
||||
var apply = Stream(function(value) {return String(value)})
|
||||
var stream = Stream(undefined)
|
||||
var applied = stream["fantasy-land/ap"](apply)
|
||||
|
||||
o(applied()).equals("undefined")
|
||||
|
||||
apply(function(value) {return String(value) + "a"})
|
||||
|
||||
o(applied()).equals("undefineda")
|
||||
})
|
||||
})
|
||||
o.spec("fantasy-land", function() {
|
||||
o.spec("functor", function() {
|
||||
o("identity", function() {
|
||||
var stream = Stream(3)
|
||||
var mapped = stream["fantasy-land/map"](function(value) {return value})
|
||||
|
||||
o(stream()).equals(mapped())
|
||||
})
|
||||
o("composition", function() {
|
||||
function f(x) {return x * 2}
|
||||
function g(x) {return x * x}
|
||||
|
||||
var stream = Stream(3)
|
||||
|
||||
var mapped = stream["fantasy-land/map"](function(value) {return f(g(value))})
|
||||
var composed = stream["fantasy-land/map"](g)["fantasy-land/map"](f)
|
||||
|
||||
o(mapped()).equals(18)
|
||||
o(mapped()).equals(composed())
|
||||
})
|
||||
})
|
||||
o.spec("apply", function() {
|
||||
o("composition", function() {
|
||||
var a = Stream(function(value) {return value * 2})
|
||||
var u = Stream(function(value) {return value * 3})
|
||||
var v = Stream(5)
|
||||
|
||||
var mapped = v["fantasy-land/ap"](u["fantasy-land/ap"](a["fantasy-land/map"](function(f) {
|
||||
return function(g) {
|
||||
return function(x) {
|
||||
return f(g(x))
|
||||
}
|
||||
}
|
||||
})))
|
||||
|
||||
var composed = v["fantasy-land/ap"](u)["fantasy-land/ap"](a)
|
||||
|
||||
o(mapped()).equals(30)
|
||||
o(mapped()).equals(composed())
|
||||
})
|
||||
})
|
||||
o.spec("applicative", function() {
|
||||
o("identity", function() {
|
||||
var a = Stream()["fantasy-land/of"](function(value) {return value})
|
||||
var v = Stream(5)
|
||||
|
||||
o(v["fantasy-land/ap"](a)()).equals(5)
|
||||
o(v["fantasy-land/ap"](a)()).equals(v())
|
||||
})
|
||||
o("homomorphism", function() {
|
||||
var a = Stream(0)
|
||||
var f = function(value) {return value * 2}
|
||||
var x = 3
|
||||
|
||||
o(a["fantasy-land/of"](x)["fantasy-land/ap"](a["fantasy-land/of"](f))()).equals(6)
|
||||
o(a["fantasy-land/of"](x)["fantasy-land/ap"](a["fantasy-land/of"](f))()).equals(a["fantasy-land/of"](f(x))())
|
||||
})
|
||||
o("interchange", function() {
|
||||
var u = Stream(function(value) {return value * 2})
|
||||
var a = Stream()
|
||||
var y = 3
|
||||
|
||||
o(a["fantasy-land/of"](y)["fantasy-land/ap"](u)()).equals(6)
|
||||
o(a["fantasy-land/of"](y)["fantasy-land/ap"](u)()).equals(u["fantasy-land/ap"](a["fantasy-land/of"](function(f) {return f(y)}))())
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
205
util/stream.js
205
util/stream.js
|
|
@ -1,205 +0,0 @@
|
|||
"use strict"
|
||||
|
||||
module.exports = function(log) {
|
||||
var guid = 0, noop = function() {}, HALT = {}
|
||||
function createStream() {
|
||||
function stream() {
|
||||
if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, arguments[0], undefined)
|
||||
return stream._state.value
|
||||
}
|
||||
initStream(stream)
|
||||
|
||||
if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, arguments[0], undefined)
|
||||
|
||||
return stream
|
||||
}
|
||||
function initStream(stream) {
|
||||
stream.constructor = createStream
|
||||
stream._state = {id: guid++, value: undefined, error: undefined, state: 0, derive: undefined, recover: undefined, deps: {}, parents: [], errorStream: undefined, endStream: undefined}
|
||||
stream.map = stream["fantasy-land/map"] = map, stream["fantasy-land/ap"] = ap, stream["fantasy-land/of"] = createStream
|
||||
stream.valueOf = valueOf, stream.toJSON = toJSON, stream.toString = valueOf
|
||||
stream.run = run, stream.catch = doCatch
|
||||
|
||||
Object.defineProperties(stream, {
|
||||
error: {get: function() {
|
||||
if (!stream._state.errorStream) {
|
||||
var errorStream = function() {
|
||||
if (arguments.length > 0 && arguments[0] !== HALT) updateStream(stream, undefined, arguments[0])
|
||||
return stream._state.error
|
||||
}
|
||||
initStream(errorStream)
|
||||
initDependency(errorStream, [stream], noop, noop)
|
||||
stream._state.errorStream = errorStream
|
||||
}
|
||||
return stream._state.errorStream
|
||||
}},
|
||||
end: {get: function() {
|
||||
if (!stream._state.endStream) {
|
||||
var endStream = createStream()
|
||||
endStream.map(function(value) {
|
||||
if (value === true) unregisterStream(stream), unregisterStream(endStream)
|
||||
return value
|
||||
})
|
||||
stream._state.endStream = endStream
|
||||
}
|
||||
return stream._state.endStream
|
||||
}}
|
||||
})
|
||||
}
|
||||
function updateStream(stream, value, error) {
|
||||
updateState(stream, value, error)
|
||||
for (var id in stream._state.deps) updateDependency(stream._state.deps[id], false)
|
||||
finalize(stream)
|
||||
}
|
||||
function updateState(stream, value, error) {
|
||||
error = unwrapError(value, error)
|
||||
if (error !== undefined && typeof stream._state.recover === "function") {
|
||||
if (!resolve(stream, updateValues, true)) return
|
||||
}
|
||||
else updateValues(stream, value, error)
|
||||
stream._state.changed = true
|
||||
if (stream._state.state !== 2) stream._state.state = 1
|
||||
}
|
||||
function updateValues(stream, value, error) {
|
||||
stream._state.value = value
|
||||
stream._state.error = error
|
||||
}
|
||||
function updateDependency(stream, mustSync) {
|
||||
var state = stream._state, parents = state.parents
|
||||
if (parents.length > 0 && parents.filter(active).length === parents.length && (mustSync || parents.filter(changed).length > 0)) {
|
||||
var failed = parents.filter(errored)
|
||||
if (failed.length > 0) updateState(stream, undefined, failed[0]._state.error)
|
||||
else resolve(stream, updateState, false)
|
||||
}
|
||||
}
|
||||
function resolve(stream, update, shouldRecover) {
|
||||
try {
|
||||
var value = shouldRecover ? stream._state.recover() : stream._state.derive()
|
||||
if (value === HALT) return false
|
||||
update(stream, value, undefined)
|
||||
}
|
||||
catch (e) {
|
||||
update(stream, undefined, e.__error != null ? e.__error : e)
|
||||
if (e.__error == null) reportUncaughtError(stream, e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
function unwrapError(value, error) {
|
||||
if (value != null && value.constructor === createStream) {
|
||||
if (value._state.error !== undefined) error = value._state.error
|
||||
else error = unwrapError(value._state.value, value._state.error)
|
||||
}
|
||||
return error
|
||||
}
|
||||
function finalize(stream) {
|
||||
stream._state.changed = false
|
||||
for (var id in stream._state.deps) stream._state.deps[id]._state.changed = false
|
||||
}
|
||||
function reportUncaughtError(stream, e) {
|
||||
if (Object.keys(stream._state.deps).length === 0) {
|
||||
setTimeout(function() {
|
||||
if (Object.keys(stream._state.deps).length === 0) log(e)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
function run(fn) {
|
||||
var self = createStream(), stream = this
|
||||
return initDependency(self, [stream], function() {
|
||||
return absorb(self, fn(stream()))
|
||||
}, undefined)
|
||||
}
|
||||
function doCatch(fn) {
|
||||
var self = createStream(), stream = this
|
||||
var derive = function() {return stream._state.value}
|
||||
var recover = function() {return absorb(self, fn(stream._state.error))}
|
||||
return initDependency(self, [stream], derive, recover)
|
||||
}
|
||||
function combine(fn, streams) {
|
||||
if (streams.length > streams.filter(valid).length) throw new Error("Ensure that each item passed to m.prop.combine/m.prop.merge is a stream")
|
||||
return initDependency(createStream(), streams, function() {
|
||||
var failed = streams.filter(errored)
|
||||
if (failed.length > 0) throw {__error: failed[0]._state.error}
|
||||
return fn.apply(this, streams.concat([streams.filter(changed)]))
|
||||
}, undefined)
|
||||
}
|
||||
function absorb(stream, value) {
|
||||
if (value != null && value.constructor === createStream) {
|
||||
var absorbable = value
|
||||
var update = function() {
|
||||
updateState(stream, absorbable._state.value, absorbable._state.error)
|
||||
for (var id in stream._state.deps) updateDependency(stream._state.deps[id], false)
|
||||
}
|
||||
absorbable["fantasy-land/map"](update).catch(function(e) {
|
||||
update()
|
||||
throw {__error: e}
|
||||
})
|
||||
|
||||
if (absorbable._state.state === 0) return HALT
|
||||
if (absorbable._state.error) throw {__error: absorbable._state.error}
|
||||
value = absorbable._state.value
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function initDependency(dep, streams, derive, recover) {
|
||||
var state = dep._state
|
||||
state.derive = derive
|
||||
state.recover = recover
|
||||
state.parents = streams.filter(notEnded)
|
||||
|
||||
registerDependency(dep, state.parents)
|
||||
updateDependency(dep, true)
|
||||
|
||||
return dep
|
||||
}
|
||||
function registerDependency(stream, parents) {
|
||||
for (var i = 0; i < parents.length; i++) {
|
||||
parents[i]._state.deps[stream._state.id] = stream
|
||||
registerDependency(stream, parents[i]._state.parents)
|
||||
}
|
||||
}
|
||||
function unregisterStream(stream) {
|
||||
for (var i = 0; i < stream._state.parents.length; i++) {
|
||||
var parent = stream._state.parents[i]
|
||||
delete parent._state.deps[stream._state.id]
|
||||
}
|
||||
for (var id in stream._state.deps) {
|
||||
var dependent = stream._state.deps[id]
|
||||
var index = dependent._state.parents.indexOf(stream)
|
||||
if (index > -1) dependent._state.parents.splice(index, 1)
|
||||
}
|
||||
stream._state.state = 2 //ended
|
||||
stream._state.deps = {}
|
||||
}
|
||||
|
||||
function map(fn) {return combine(function(stream) {return fn(stream())}, [this])}
|
||||
function ap(stream) {return combine(function(s1, s2) {return s1()(s2())}, [stream, this])}
|
||||
function valueOf() {return this._state.value}
|
||||
function toJSON() {return this._state.value != null && typeof this._state.value.toJSON === "function" ? this._state.value.toJSON() : this._state.value}
|
||||
|
||||
function valid(stream) {return stream._state }
|
||||
function active(stream) {return stream._state.state === 1}
|
||||
function changed(stream) {return stream._state.changed}
|
||||
function notEnded(stream) {return stream._state.state !== 2}
|
||||
function errored(stream) {return stream._state.error}
|
||||
|
||||
function reject(e) {
|
||||
var stream = createStream()
|
||||
stream.error(e)
|
||||
return stream
|
||||
}
|
||||
|
||||
function merge(streams) {
|
||||
return combine(function () {
|
||||
return streams.map(function(s) {return s()})
|
||||
}, streams)
|
||||
}
|
||||
createStream["fantasy-land/of"] = createStream
|
||||
createStream.merge = merge
|
||||
createStream.combine = combine
|
||||
createStream.reject = reject
|
||||
createStream.HALT = HALT
|
||||
|
||||
return createStream
|
||||
}
|
||||
|
|
@ -6,12 +6,9 @@
|
|||
<body>
|
||||
<script src="../../module/module.js"></script>
|
||||
<script src="../../ospec/ospec.js"></script>
|
||||
<script src="../../test-utils/callAsync.js"></script>
|
||||
|
||||
<script src="../../util/withAttr.js"></script>
|
||||
<script src="../../util/stream.js"></script>
|
||||
<script src="test-withAttr.js"></script>
|
||||
<script src="test-stream.js"></script>
|
||||
|
||||
<script>require("../../ospec/ospec").run()</script>
|
||||
</body>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue