diff --git a/CHANGELOG.md b/CHANGELOG.md index fe0f76e..7102edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Karet XHR Changelog +## 0.7.0 + +Renamed +* `upHasFailed` to `upHasErrored`, +* `downHasFailed` to `downHasErrored`, and +* `hasFailed` to `hasErrored` +and also introduced new function +* `hasFailed`. + +`template` and `apply` were changed to process XHRs in parallel using the newly +added `IdentityParallel` algebra, because that is what one more often wants. +Note that this doesn't change the behaviour of the `IdentitySucceeded` algebra. + +## 0.6.7 + +`apply` was changed to accept both XHRs and plain values. + ## 0.6.2 Fix work around for IE11 where `'progress'` events may be sent after `'load'` diff --git a/README.md b/README.md index 4477ca4..952b48f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ # [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#karet-xhr) [Karet XHR](#karet-xhr) · [![Gitter](https://img.shields.io/gitter/room/calmm-js/chat.js.svg)](https://gitter.im/calmm-js/chat) [![GitHub stars](https://img.shields.io/github/stars/calmm-js/karet.xhr.svg?style=social)](https://github.com/calmm-js/karet.xhr) -This library provides a thin wrapper over the standard -[`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) -API allowing one to perform XHRs by observing the state of ongoing requests as -observable [Kefir](https://kefirjs.github.io/kefir/) -[properties](https://kefirjs.github.io/kefir/#about-observables). The benefit -of this approach is that it makes it easy to implement many kinds of use cases -ranging from just getting the response data to visualizing the progress of -ongoing upload and/or download and displaying potential errors. See also [Karet -FR](https://github.com/calmm-js/karet.fr). +This library allows one to [*declare*](#declare) +[`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)s, +[*compose*](#compose) them, and [*observe*](#observe) them through +[Kefir](https://kefirjs.github.io/kefir/) +[properties](https://kefirjs.github.io/kefir/#about-observables). This makes it +easy to implement many kinds of use cases ranging from just getting the response +data to visualizing the progress of non-trivial compositions of ongoing upload +and/or download requests and displaying potential errors. Examples: * The [Giphy](https://codesandbox.io/s/q9j8v8w1nq) CodeSandbox uses this library @@ -26,86 +25,86 @@ Examples: ## [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#contents) [Contents](#contents) * [Reference](#reference) - * [Convenience](#convenience) - * [`XHR.getJson(url | {url[, ...]}) ~> varies`](#XHR-getJson) - * [`XHR.performJson(url | {url[, ...]}) ~> xhr`](#XHR-performJson) - * [`XHR.performWith(url | {...}, url | {...}) ~> xhr`](#XHR-performWith) - * [`XHR.hasSucceeded(xhr) ~> boolean`](#XHR-hasSucceeded) - * [Starting](#starting) - * [`XHR.perform(url | {url[, method, user, password, headers, overrideMimeType, body, responseType, timeout, withCredentials]}) ~> xhr`](#XHR-perform) - * [Overall state](#overall-state) - * [`XHR.isXHR(any) ~> boolean`](#XHR-isXHR) - * [Progression](#progression) - * [`XHR.isDone(xhr) ~> boolean`](#XHR-isDone) - * [`XHR.isProgressing(xhr) ~> boolean`](#XHR-isProgressing) - * [`XHR.isStatusAvailable(xhr) ~> boolean`](#XHR-isStatusAvailable) - * [End state](#end-state) - * [`XHR.hasFailed(xhr) ~> boolean`](#XHR-hasFailed) - * [`XHR.hasTimedOut(xhr) ~> boolean`](#XHR-hasTimedOut) - * [Errors on failure](#errors-on-failure) - * [`XHR.errors(xhr) ~> [...exceptions]`](#XHR-errors) - * [Request status](#request-status) - * [`XHR.status(xhr) ~> number`](#XHR-status) - * [`XHR.statusIsHttpSuccess(xhr) ~> boolean`](#XHR-statusIsHttpSuccess) - * [`XHR.statusText(xhr) ~> string`](#XHR-statusText) - * [Data transfer](#data-transfer) - * [`XHR.loaded(xhr) ~> number`](#XHR-loaded) - * [`XHR.total(xhr) ~> number`](#XHR-total) - * [Response headers](#response-headers) - * [`XHR.allResponseHeaders(xhr) ~> string`](#XHR-allResponseHeaders) - * [`XHR.responseHeader(header, xhr) ~> string`](#XHR-responseHeader) - * [Response data](#response-data) - * [`XHR.response(xhr) ~> varies`](#XHR-response) - * [`XHR.responseText(xhr) ~> string`](#XHR-responseText) - * [`XHR.responseXML(xhr) ~> document`](#XHR-responseXML) - * [Response URL](#response-url) - * [`XHR.responseURL(xhr) ~> string`](#XHR-responseURL) - * [Request parameters](#request-parameters) - * [`XHR.responseType(xhr) ~> string`](#XHR-responseType) - * [`XHR.timeout(xhr) ~> number`](#XHR-timeout) - * [`XHR.withCredentials(xhr) ~> boolean`](#XHR-withCredentials) - * [Ready state](#ready-state) - * [`XHR.readyState(xhr) ~> number`](#XHR-readyState) - * [Download state](#download-state) - * [`XHR.downError(xhr) ~> exception`](#XHR-downError) - * [`XHR.downHasCompleted(xhr) ~> boolean`](#XHR-downHasCompleted) - * [`XHR.downHasEnded(xhr) ~> boolean`](#XHR-downHasEnded) - * [`XHR.downHasFailed(xhr) ~> boolean`](#XHR-downHasFailed) - * [`XHR.downHasStarted(xhr) ~> boolean`](#XHR-downHasStarted) - * [`XHR.downHasTimedOut(xhr) ~> boolean`](#XHR-downHasTimedOut) - * [`XHR.downIsProgressing(xhr) ~> boolean`](#XHR-downIsProgressing) - * [`XHR.downLoaded(xhr) ~> number`](#XHR-downLoaded) - * [`XHR.downTotal(xhr) ~> number`](#XHR-downTotal) - * [Upload state](#upload-state) - * [`XHR.upError(xhr) ~> exception`](#XHR-upError) - * [`XHR.upHasCompleted(xhr) ~> boolean`](#XHR-upHasCompleted) - * [`XHR.upHasEnded(xhr) ~> boolean`](#XHR-upHasEnded) - * [`XHR.upHasFailed(xhr) ~> boolean`](#XHR-upHasFailed) - * [`XHR.upHasStarted(xhr) ~> boolean`](#XHR-upHasStarted) - * [`XHR.upHasTimedOut(xhr) ~> boolean`](#XHR-upHasTimedOut) - * [`XHR.upIsProgressing(xhr) ~> boolean`](#XHR-upIsProgressing) - * [`XHR.upLoaded(xhr) ~> number`](#XHR-upLoaded) - * [`XHR.upTotal(xhr) ~> number`](#XHR-upTotal) - * [Happy path](#happy-path) - * [`XHR.result(xhr) ~> varies`](#XHR-result) - * [Happy path algebras](#happy-path-algebras) - * [`XHR.IdentitySucceeded ~> Monad`](#XHR-IdentitySucceeded) - * [`XHR.Succeeded ~> Monad`](#XHR-Succeeded) - * [Monadic happy path combinators](#monadic-happy-path-combinators) + * [Just give me the data!](#just-give-me-the-data) + * [`XHR.getJson(url | {url[, ...options]}) ~> varies`](#XHR-getJson) + * [Declare](#declare) + * [`XHR.perform(url | {url[, ...options]}) ~> xhr`](#XHR-perform) + * [`XHR.performJson(url | {url[, ...options]}) ~> xhr`](#XHR-performJson) + * [`XHR.performWith(url | {...options}, url | {...options}) ~> xhr`](#XHR-performWith) + * [Compose](#compose) + * [Basic combinators](#basic-combinators) * [`XHR.ap(xhrAtoB, xhrA) ~> xhrB`](#XHR-ap) - * [`XHR.chain(responseA => xhrB, xhrA) ~> xhrB`](#XHR-chain) - * [`XHR.map(responseA => responseB, xhrA) ~> xhrB`](#XHR-map) - * [`XHR.of(response) ~> xhr`](#XHR-of) - * [Additional happy path combinators](#additional-happy-path-combinators) - * [`XHR.apply((...responseAs) => responseB, [...xhrAs]) ~> xhrB`](#XHR-apply) + * [`XHR.chain(A => xhrB, xhrA) ~> xhrB`](#XHR-chain) + * [`XHR.map(A => B, xhrA) ~> xhrB`](#XHR-map) + * [`XHR.of(A) ~> xhrA`](#XHR-of) + * [Additional combinators](#additional-combinators) + * [`XHR.apply((...As) => B, [...xhrAs]) ~> xhrB`](#XHR-apply) * [`XHR.template([ ... xhr ... ] | { ... xhr ... }) ~> xhr`](#XHR-template) + * [Algebras](#algebras) + * [`XHR.IdentityParallel ~> applicative`](#XHR-IdentityParallel) + * [`XHR.IdentitySucceeded ~> monad`](#XHR-IdentitySucceeded) + * [`XHR.Parallel ~> applicative`](#XHR-Parallel) + * [`XHR.Succeeded ~> monad`](#XHR-Succeeded) + * [Observe](#observe) + * [Result](#result) + * [`XHR.hasFailed(xhr) ~> boolean`](#XHR-hasFailed) + * [`XHR.hasSucceeded(xhr) ~> boolean`](#XHR-hasSucceeded) + * [`XHR.result(xhrA) ~> A`](#XHR-result) + * [Overall state](#overall-state) + * [Progression](#progression) + * [`XHR.isDone(xhr) ~> boolean`](#XHR-isDone) + * [`XHR.isProgressing(xhr) ~> boolean`](#XHR-isProgressing) + * [`XHR.isStatusAvailable(xhr) ~> boolean`](#XHR-isStatusAvailable) + * [End state](#end-state) + * [`XHR.hasErrored(xhr) ~> boolean`](#XHR-hasErrored) + * [`XHR.hasTimedOut(xhr) ~> boolean`](#XHR-hasTimedOut) + * [Errors on failure](#errors-on-failure) + * [`XHR.errors(xhr) ~> [...exceptions]`](#XHR-errors) + * [Request status](#request-status) + * [`XHR.status(xhr) ~> number`](#XHR-status) + * [`XHR.statusIsHttpSuccess(xhr) ~> boolean`](#XHR-statusIsHttpSuccess) + * [`XHR.statusText(xhr) ~> string`](#XHR-statusText) + * [Data transfer](#data-transfer) + * [`XHR.loaded(xhr) ~> number`](#XHR-loaded) + * [`XHR.total(xhr) ~> number`](#XHR-total) + * [Response headers](#response-headers) + * [`XHR.allResponseHeaders(xhr) ~> string`](#XHR-allResponseHeaders) + * [`XHR.responseHeader(header, xhr) ~> string`](#XHR-responseHeader) + * [Response data](#response-data) + * [`XHR.response(xhr) ~> varies`](#XHR-response) + * [`XHR.responseText(xhr) ~> string`](#XHR-responseText) + * [`XHR.responseXML(xhr) ~> document`](#XHR-responseXML) + * [Response URL](#response-url) + * [`XHR.responseURL(xhr) ~> string`](#XHR-responseURL) + * [Request parameters](#request-parameters) + * [`XHR.responseType(xhr) ~> string`](#XHR-responseType) + * [`XHR.timeout(xhr) ~> number`](#XHR-timeout) + * [`XHR.withCredentials(xhr) ~> boolean`](#XHR-withCredentials) + * [Ready state](#ready-state) + * [`XHR.readyState(xhr) ~> number`](#XHR-readyState) + * [Download state](#download-state) + * [`XHR.downError(xhr) ~> exception`](#XHR-downError) + * [`XHR.downHasCompleted(xhr) ~> boolean`](#XHR-downHasCompleted) + * [`XHR.downHasEnded(xhr) ~> boolean`](#XHR-downHasEnded) + * [`XHR.downHasErrored(xhr) ~> boolean`](#XHR-downHasErrored) + * [`XHR.downHasStarted(xhr) ~> boolean`](#XHR-downHasStarted) + * [`XHR.downHasTimedOut(xhr) ~> boolean`](#XHR-downHasTimedOut) + * [`XHR.downIsProgressing(xhr) ~> boolean`](#XHR-downIsProgressing) + * [`XHR.downLoaded(xhr) ~> number`](#XHR-downLoaded) + * [`XHR.downTotal(xhr) ~> number`](#XHR-downTotal) + * [Upload state](#upload-state) + * [`XHR.upError(xhr) ~> exception`](#XHR-upError) + * [`XHR.upHasCompleted(xhr) ~> boolean`](#XHR-upHasCompleted) + * [`XHR.upHasEnded(xhr) ~> boolean`](#XHR-upHasEnded) + * [`XHR.upHasErrored(xhr) ~> boolean`](#XHR-upHasErrored) + * [`XHR.upHasStarted(xhr) ~> boolean`](#XHR-upHasStarted) + * [`XHR.upHasTimedOut(xhr) ~> boolean`](#XHR-upHasTimedOut) + * [`XHR.upIsProgressing(xhr) ~> boolean`](#XHR-upIsProgressing) + * [`XHR.upLoaded(xhr) ~> number`](#XHR-upLoaded) + * [`XHR.upTotal(xhr) ~> number`](#XHR-upTotal) * [Auxiliary](#auxiliary) * [`XHR.isHttpSuccess(number) ~> boolean`](#XHR-isHttpSuccess) - * [Deprecated](#deprecated) - * ~~[`XHR.downHasSucceeded(xhr) ~> boolean`](#XHR-downHasSucceeded)~~ - * ~~[`XHR.headersReceived(xhr) ~> boolean`](#XHR-headersReceived)~~ - * ~~[`XHR.responseFull(xhr) ~> varies`](#XHR-responseFull)~~ - * ~~[`XHR.upHasSucceeded(xhr) ~> boolean`](#XHR-upHasSucceeded)~~ + * [`XHR.isXHR(any) ~> boolean`](#XHR-isXHR) ## [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#reference) [Reference](#reference) @@ -116,52 +115,44 @@ imports the library as: import * as XHR from 'karet.xhr' ``` -Using this library, one first [creates](#starting) an observable property -representing the state of an -[`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) -using [`XHR.perform`](#XHR-perform) and then observes the ongoing XHR state -using the accessors for [overall](#overall-state), [download](#download-state), -and [upload](#upload-state) state. +Using this library, one [declares](#declare) observable +[`XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)s, +[composes](#compose) them, and then [observes](#observe) the ongoing XHR using +the accessors for the [result](#result), [overall](#overall-state), +[download](#download-state), and [upload](#upload-state) state. -### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#convenience) [Convenience](#convenience) +### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#just-give-me-the-data) [Just give me the data!](#just-give-me-the-data) -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-getJson) [`XHR.getJson(url | {url,[, ...]}) ~> varies`](#XHR-getJson) +If you just want to GET some JSON... -`XHR.getJson(arg)` returns an observable that emits the [full -response](#XHR-result) after the [XHR has succeeded](#XHR-hasSucceeded). In -case the XHR fails or times out, the XHR is emitted as an error. +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-getJson) [`XHR.getJson(url | {url,[, ...options]}) ~> varies`](#XHR-getJson) -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-peformJson) [`XHR.performJson(url | {url[, ...]}) ~> xhr`](#XHR-performJson) +`XHR.getJson` returns an observable that emits the [full response](#XHR-result) +after the [XHR has succeeded](#XHR-hasSucceeded). In case the XHR produces an +error or times out, the XHR is emitted as an error event. See +[`XHR.perform`](#XHR-perform) for the options. -`XHR.performJson` is shorthand for [`XHR.performWith({responseType: -'json', headers: {'Content-Type': 'application/json'}})`](#XHR-performWith). - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-performWith) [`XHR.performWith(url | {...}, url | {...}) ~> xhr`](#XHR-performWith) - -`XHR.performWith` is a curried function that allows one to define a -[`XHR.perform`](#XHR-perform) like function with default parameters. The -defaults (first parameter) are merged with the overrides (second parameter). -Headers are also merged. See [`XHR.perform`](#XHR-perform) for the parameters. +Note that this function is provided for simplistic usages where one does not +need the full composability and observability advantages of this library. For example: -```jsx -const get = XHR.performWith({responseType: 'json', timeout: 30*1000}) -// ... -get(url) +```js +I.seq( + XHR.getJson(`https://api.github.com/search/users?q=polytypic`), + R.map(L.get(L.query('html_url'))), + log +) ``` -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-hasSucceeded) [`XHR.hasSucceeded(xhr) ~> boolean`](#XHR-hasSucceeded) - -`XHR.hasSucceeded` returns a possibly observable boolean property of an ongoing -XHR that is true if the XHR [is done](#XHR-isDone), its HTTP [status indicates -success](#XHR-statusIsHttpSuccess), and neither -[download](#XHR-downHasCompleted) or [upload](#XHR-upHasCompleted) has failed or -timed out. +### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#declare) [Declare](#declare) -### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#starting) [Starting](#starting) +XHRs are declared by specifying all the parameters that affect the execution of +an XHR to [`XHR.perform`](#XHR-perform), which then returns an observable +[property](https://kefirjs.github.io/kefir/#about-observables) that can be +subscribed to in order to perform the declared XHR. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-perform) [`XHR.perform(url | {url[, method, user, password, headers, overrideMimeType, body, responseType, timeout, withCredentials]}) ~> xhr`](#XHR-perform) +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-perform) [`XHR.perform(url | {url[, ...options]}) ~> xhr`](#XHR-perform) `XHR.perform` creates an observable [property](https://kefirjs.github.io/kefir/#about-observables) that represents @@ -170,7 +161,7 @@ the state of an ongoing The request is started once the property is subscribed to and is automatically [aborted](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort) in case the property is fully unsubscribed from before it has ended. See also -[`XHR.performWith`](#XHR-performWith). +[`XHR.performWith`](#XHR-performWith) and [`XHR.performJson`](#XHR-performJson). Only the `url` parameter is required and can be passed as a string. Other parameters have their XHR default values: @@ -193,8 +184,8 @@ created by `XHR.perform` performs the XHR with the [latest](https://kefirjs.github.io/kefir/#flat-map-latest) argument values. Note that typically one does not explicitly subscribe to the property, but one -rather computes a desired view of the property, such as a view of the -[response](#XHR-response), and combines that further into some more interesting +rather computes a desired view of the property, such as a view of the succeeded +[response](#XHR-result), and combines that further into some more interesting property. WARNING: Setting `responseType` to `'json'` is not supported by IE 11. This @@ -202,19 +193,173 @@ library implements a workaround by calling `JSON.parse` on the returned data in case setting `responseType` to `'json'` fails. In case the response does not parse, then [`XHR.response`](#XHR-response) returns `null`. -See this live [GitHub repository search](https://codesandbox.io/s/l5271q0r2l) -CodeSandbox for an example. +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-peformJson) [`XHR.performJson(url | {url[, ...options]}) ~> xhr`](#XHR-performJson) -### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#overall-state) [Overall state](#overall-state) +`XHR.performJson` is shorthand for [`XHR.performWith({responseType: +'json', headers: {'Content-Type': 'application/json'}})`](#XHR-performWith). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isXHR) [`XHR.isXHR(any) ~> boolean`](#XHR-isXHR) +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-performWith) [`XHR.performWith(url | {...options}, url | {...options}) ~> xhr`](#XHR-performWith) -`XHR.isXHR` returns a possibly observable boolean property that tells whether -the given value is a XHR. +`XHR.performWith` is a curried function that allows one to define a +[`XHR.perform`](#XHR-perform) like function with default parameters. The +defaults (first parameter) are merged with the overrides (second parameter). +Headers are also merged. See [`XHR.perform`](#XHR-perform) for the parameters. + +For example: + +```jsx +const get = XHR.performWith({responseType: 'json', timeout: 30*100e +// ..eete(url) +``` + +### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#compose) [Compose](#compose) + +Multiple XHRs can be composed together to appear and be treated simply as a +single XHR. + +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#basic-combinators) [Basic combinators](#basic-combinators) + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-ap) [`XHR.ap(xhrAtoB, xhrA) ~> xhrB`](#XHR-ap) + +`XHR.ap` implements a static land compatible +[`ap`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#apply) +function for composing succeeding XHRs. The XHRs are performed sequentially. +See also [`XHR.apply`](#XHR-apply). + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-chain) [`XHR.chain(A => xhrB, xhrA) ~> xhrB`](#XHR-chain) + +`XHR.chain` implements a static land compatible +[`chain`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#chain) +function for composing succeeding XHRs. + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-map) [`XHR.map(A => B, xhrA) ~> xhrB`](#XHR-map) + +`XHR.map` implements a static land compatible +[`map`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#functor) +function for composing succeeding XHRs. + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-of) [`XHR.of(A) ~> xhrA`](#XHR-of) + +`XHR.of` implements a static land compatible +[`of`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#applicative) +function for composing succeeding XHRs. + +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#additional-combinators) [Additional combinators](#additional-combinators) + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-apply) [`XHR.apply((...As) => B, [...xhrAs]) ~> xhrB`](#XHR-apply) + +`XHR.apply` maps the given XHRs through the given function. Unlike with +[`XHR.ap`](#XHR-ap), the XHRs are performed in parallel. + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-template) [`XHR.template([ ... xhr ... ] | { ... xhr ... }) ~> xhr`](#XHR-template) + +`XHR.template` transforms a nested template of plain arrays and objects possibly +containing XHRs into a XHR. The XHRs are performed in parallel. + +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#algebras) [Algebras](#algebras) -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#progression) [Progression](#progression) +[Static Land](https://github.com/rpominov/static-land) compatible algebras can +be used with other Static Land compatible libraries such as [Partial +Lenses](https://github.com/calmm-js/partial.lenses/) to perform more complex +XHRs. -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isStatusAvailable) [`XHR.isStatusAvailable(xhr) ~> boolean`](#XHR-isStatusAvailable) +For example: + +```js +I.seq( + XHR.performJson(`https://api.github.com/search/repositories?q=user:calmm-js&sort=stars`), + XHR.map( + L.collect([ + 'items', + L.limit(2, L.flat(L.when(R.has('description')))), + L.pick({ + description: 'description', + url: 'svn_url', + issues: 'issues_url' + }) + ]) + ), + XHR.chain( + L.traverse( + XHR.Parallel, + issues => I.seq( + XHR.performJson(issues.replace(/{.*}$/, '')), + XHR.map( + L.collect( + L.limit(3, L.flat(L.pick({title: 'title', url: 'html_url'}))) + ) + ) + ), + [L.elems, 'issues'] + ) + ), + XHR.result, + log +) +``` + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-IdentityParallel) [`XHR.IdentityParallel ~> applicative`](#XHR-IdentityParallel) + +`XHR.IdentityParallel` is a static land compatible +[applicative](https://github.com/rpominov/static-land/blob/master/docs/spec.md#applicative) +that manipulates XHRs like [`XHR.Parallel`](#XHR-Parallel) or plain data. + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-IdentitySucceeded) [`XHR.IdentitySucceeded ~> monad`](#XHR-IdentitySucceeded) + +`XHR.IdentitySucceeded` is a static land compatible +[monad](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad) +that manipulates XHRs like [`XHR.Succeeded`](#XHR-Succeeded) or plain data. + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-Parallel) [`XHR.Parallel ~> applicative`](#XHR-Parallel) + +`XHR.Parallel` is a static land compatible +[applicative](https://github.com/rpominov/static-land/blob/master/docs/spec.md#applicative) +that allows one to compose parallel XHR requests. In case any XHR fails, the +composed XHR produces the first failed XHR. In case all XHRs succeed, the +composed XHR produces the combined XHR as the [result](#XHR-result). + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-Succeeded) [`XHR.Succeeded ~> monad`](#XHR-Succeeded) + +`XHR.Succeeded` is a static land compatible +[monad](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad) +comprised of the [`XHR.ap`](#XHR-ap), [`XHR.chain`](#XHR-chain), +[`XHR.map`](#XHR-map), and [`XHR.of`](#XHR-of) combinators that allows one to +compose sequences of XHR requests that stop as soon as the first XHR does not +[succeed](#XHR-hasSucceeded). + +### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#observe) [Observe](#observe) + +Ongoing XHRs can be observed both for their varying properties such as [the +number of bytes transferred](#XHR-loaded) and for their [results](#XHR-result). + +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#result) [Result](#result) + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-hasFailed) [`XHR.hasFailed(xhr) ~> boolean`](#XHR-hasFailed) + +`XHR.hasFailed` returns a possibly observable boolean property of an ongoing XHR +that is true if its HTTP status does not indicate [success](#XHR-isHttpSuccess) +or the download or the upload operation has [errored](#XHR-hasErrored) or [timed +out](#XHR-hasTimedOut). + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-hasSucceeded) [`XHR.hasSucceeded(xhr) ~> boolean`](#XHR-hasSucceeded) + +`XHR.hasSucceeded` returns a possibly observable boolean property of an ongoing +XHR that is true if the XHR [is done](#XHR-isDone), its HTTP [status indicates +success](#XHR-statusIsHttpSuccess), and neither +[download](#XHR-downHasCompleted) or [upload](#XHR-upHasCompleted) has errored +or timed out. + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-result) [`XHR.result(xhrA) ~> A`](#XHR-result) + +`XHR.result` returns the response of a [succeeded](#XHR-hasSucceeded) XHR. Note +that [`XHR.response`](#XHR-response) allows one to obtain the response before +the XHR [is done](#XHR-isDone) and even when the XHR has (partially) failed. + +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#overall-state) [Overall state](#overall-state) + +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#progression) [Progression](#progression) + +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isStatusAvailable) [`XHR.isStatusAvailable(xhr) ~> boolean`](#XHR-isStatusAvailable) `XHR.isStatusAvailable` returns a possibly observable boolean property that tells whether HTTP status and response headers have been received and can be @@ -223,49 +368,49 @@ obtained. See also [`XHR.status`](#XHR-status), [`XHR.allResponseHeaders`](#XHR-allResponseHeaders), and [`XHR.responseHeader`](#XHR-responseHeader). -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isDone) [`XHR.isDone(xhr) ~> boolean`](#XHR-isDone) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isDone) [`XHR.isDone(xhr) ~> boolean`](#XHR-isDone) `XHR.isDone` returns a possibly observable boolean property that tells whether the XHR operation is complete (whether success or failure). See also [`XHR.hasSucceeded`](#XHR-hasSucceeded). -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isProgressing) [`XHR.isProgressing(xhr) ~> boolean`](#XHR-isProgressing) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isProgressing) [`XHR.isProgressing(xhr) ~> boolean`](#XHR-isProgressing) `XHR.isProgressing` returns a possibly observable boolean property that tells whether the XHR operation has started, but has not yet [ended](#XHR-isDone). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#end-state) [End state](#end-state) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#end-state) [End state](#end-state) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-hasFailed) [`XHR.hasFailed(xhr) ~> boolean`](#XHR-hasFailed) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-hasErrored) [`XHR.hasErrored(xhr) ~> boolean`](#XHR-hasErrored) -`XHR.hasFailed` returns a possibly observable boolean property of an ongoing XHR -that is true when either [download](#XHR-downHasFailed) or -[upload](#XHR-upHasFailed) has failed. +`XHR.hasErrored` returns a possibly observable boolean property of an ongoing +XHR that is true when either [download](#XHR-downHasErrored) or +[upload](#XHR-upHasErrored) has errored. -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-hasTimedOut) [`XHR.hasTimedOut(xhr) ~> boolean`](#XHR-hasTimedOut) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-hasTimedOut) [`XHR.hasTimedOut(xhr) ~> boolean`](#XHR-hasTimedOut) `XHR.hasTimedOut` returns a possibly observable boolean property of an ongoing XHR that is true when either [download](#XHR-downHasTimedOut) or [upload](#XHR-upHasTimedOut) has timed out. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#errors-on-failure) [Errors on failure](#errors-on-failure) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#errors-on-failure) [Errors on failure](#errors-on-failure) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-errors) [`XHR.errors(xhr) ~> [...exceptions]`](#XHR-errors) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-errors) [`XHR.errors(xhr) ~> [...exceptions]`](#XHR-errors) `XHR.errors` returns a possibly observable array of errors from [download](#XHR-downError) and [upload](#XHR-upError). The array will contain 0 to 2 errors. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#request-status) [Request status](#request-status) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#request-status) [Request status](#request-status) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-status) [`XHR.status(xhr) ~> number`](#XHR-status) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-status) [`XHR.status(xhr) ~> number`](#XHR-status) `XHR.status` returns a possibly observable property that emits the [`status`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/status) after the HTTP status has been received. When called on a non-observable XHR, [`readyState` must be 2](#XHR-isStatusAvailable) or an `Error` will be thrown. -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-statusIsHttpSuccess) [`XHR.statusIsHttpSuccess(xhr) ~> boolean`](#XHR-statusIsHttpSuccess) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-statusIsHttpSuccess) [`XHR.statusIsHttpSuccess(xhr) ~> boolean`](#XHR-statusIsHttpSuccess) `XHR.statusIsHttpSuccess(xhr)` is shorthand for `XHR.isHttpSuccess(XHR.status(xhr))`. Note that HTTP status is usually received @@ -273,28 +418,28 @@ before the [download](#XHR-downHasCompleted) and [upload](#XHR-upHasCompleted) phases have completed. See also [`XHR.hasSucceeded`](#XHR-hasSucceeded), [`XHR.status`](#XHR-status) and [`XHR.isHttpSuccess`](#XHR-isHttpSuccess). -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-statusText) [`XHR.statusText(xhr) ~> string`](#XHR-statusText) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-statusText) [`XHR.statusText(xhr) ~> string`](#XHR-statusText) `XHR.statusText` returns a possibly observable property of the [`statusText`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/statusText) after the HTTP status has been received. When called on a non-observable XHR, [`readyState` must be 2](#XHR-isStatusAvailable) or an `Error` will be thrown. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#data-transfer) [Data transfer](#data-transfer) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#data-transfer) [Data transfer](#data-transfer) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-loaded) [`XHR.loaded(xhr) ~> number`](#XHR-loaded) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-loaded) [`XHR.loaded(xhr) ~> number`](#XHR-loaded) `XHR.loaded` returns a possibly observable property of the sum of [downloaded](#XHR-downLoaded) and [uploaded](#XHR-upLoaded) bytes. -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-total) [`XHR.total(xhr) ~> number`](#XHR-total) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-total) [`XHR.total(xhr) ~> number`](#XHR-total) `XHR.loaded` returns a possibly observable property of the sum of [total download](#XHR-downTotal) and [total upload](#XHR-upTotal) bytes. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#response-headers) [Response headers](#response-headers) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#response-headers) [Response headers](#response-headers) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-allResponseHeaders) [`XHR.allResponseHeaders(xhr) ~> string`](#XHR-allResponseHeaders) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-allResponseHeaders) [`XHR.allResponseHeaders(xhr) ~> string`](#XHR-allResponseHeaders) `XHR.allResponseHeaders` returns a possibly observable property that emits the value of @@ -302,7 +447,7 @@ value of after the HTTP headers have been received. When called on a non-observable XHR, its [`readyState` must be 2](#XHR-isStatusAvailable) or an `Error` will be thrown. -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseHeader) [`XHR.responseHeader(header, xhr) ~> string`](#XHR-responseHeader) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseHeader) [`XHR.responseHeader(header, xhr) ~> string`](#XHR-responseHeader) `XHR.responseHeader` returns a possibly observable property that emits the value of @@ -311,9 +456,9 @@ for specified `header` after the HTTP headers have been received. When called on a non-observable XHR, its [`readyState` must be 2](#XHR-isStatusAvailable) or an `Error` will be thrown. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#response-data) [Response data](#response-data) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#response-data) [Response data](#response-data) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-response) [`XHR.response(xhr) ~> varies`](#XHR-response) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-response) [`XHR.response(xhr) ~> varies`](#XHR-response) `XHR.response` returns a possibly observable property that emits the [`response`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response) @@ -322,7 +467,7 @@ When called on a non-observable XHR, the download operation must be completed or an `Error` will be thrown. See also [`XHR.result`](#XHR-result), and [`XHR.responseText`](#XHR-responseText). -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseText) [`XHR.responseText(xhr) ~> string`](#XHR-responseText) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseText) [`XHR.responseText(xhr) ~> string`](#XHR-responseText) `XHR.responseText` returns a possibly observable property of the [`responseText`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseText) @@ -330,7 +475,7 @@ property of an ongoing XHR. `XHR.responseText` is for observing the received response data before the data has been completely received. See also [`XHR.response`](#XHR-response). -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseXML) [`XHR.responseXML(xhr) ~> document`](#XHR-responseXML) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseXML) [`XHR.responseXML(xhr) ~> document`](#XHR-responseXML) `XHR.responseXML` returns a possibly observable property of the [`responseXML`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML) @@ -338,9 +483,9 @@ property after the XHR has completed. When called on a non-observable XHR, the download operation must be completed or an `Error` will be thrown. See also [`XHR.response`](#XHR-response). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#response-url) [Response URL](#response-url) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#response-url) [Response URL](#response-url) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseURL) [`XHR.responseURL(xhr) ~> string`](#XHR-responseURL) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseURL) [`XHR.responseURL(xhr) ~> string`](#XHR-responseURL) `XHR.responseURL` returns a possibly observable property of the [`responseURL`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseURL) @@ -348,61 +493,61 @@ property after the HTTP headers have been received. When called on a non-observable XHR, its [`readyState` must be 2](#XHR-isStatusAvailable) or an `Error` will be thrown. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#request-parameters) [Request parameters](#request-parameters) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#request-parameters) [Request parameters](#request-parameters) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseType) [`XHR.responseType(xhr) ~> string`](#XHR-responseType) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseType) [`XHR.responseType(xhr) ~> string`](#XHR-responseType) `XHR.responseType` returns a possibly observable property of the [`responseType`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType) of an ongoing XHR. -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-timeout) [`XHR.timeout(xhr) ~> number`](#XHR-timeout) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-timeout) [`XHR.timeout(xhr) ~> number`](#XHR-timeout) `XHR.timeout` returns a possibly observable property of the [`timeout`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout) property of an ongoing XHR. -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-withCredentials) [`XHR.withCredentials(xhr) ~> boolean`](#XHR-withCredentials) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-withCredentials) [`XHR.withCredentials(xhr) ~> boolean`](#XHR-withCredentials) `XHR.withCredentials` returns a possibly observable property of the [`withCredentials`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials) property of an ongoing XHR. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#ready-state) [Ready state](#ready-state) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#ready-state) [Ready state](#ready-state) -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-readyState) [`XHR.readyState(xhr) ~> number`](#XHR-readyState) +###### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-readyState) [`XHR.readyState(xhr) ~> number`](#XHR-readyState) `XHR.readyState` returns a possibly observable property of the [`readyState`](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState) of an ongoing XHR. -### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#download-state) [Download state](#download-state) +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#download-state) [Download state](#download-state) -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downError) [`XHR.downError(xhr) ~> exception`](#XHR-downError) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downError) [`XHR.downError(xhr) ~> exception`](#XHR-downError) `XHR.downError` returns a possibly observable property of the [`error`](https://developer.mozilla.org/en-US/docs/Web/Events/error) property of -a failed XHR. +an errored XHR. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasEnded) [`XHR.downHasEnded(xhr) ~> boolean`](#XHR-downHasEnded) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasEnded) [`XHR.downHasEnded(xhr) ~> boolean`](#XHR-downHasEnded) `XHR.downHasEnded` returns a possibly observable boolean property that tells whether the download operation of an ongoing XHR has [ended](https://developer.mozilla.org/en-US/docs/Web/Events/loadend). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasFailed) [`XHR.downHasFailed(xhr) ~> boolean`](#XHR-downHasFailed) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasErrored) [`XHR.downHasErrored(xhr) ~> boolean`](#XHR-downHasErrored) -`XHR.downHasFailed` returns a possibly observable boolean property that tells +`XHR.downHasErrored` returns a possibly observable boolean property that tells whether the download operation of an ongoing XHR has -[failed](https://developer.mozilla.org/en-US/docs/Web/Events/error). +[errored](https://developer.mozilla.org/en-US/docs/Web/Events/error). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasStarted) [`XHR.downHasStarted(xhr) ~> boolean`](#XHR-downHasStarted) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasStarted) [`XHR.downHasStarted(xhr) ~> boolean`](#XHR-downHasStarted) `XHR.downHasStarted` returns a possibly observable boolean property that tells whether the download operation of an ongoing XHR has [started](https://developer.mozilla.org/en-US/docs/Web/Events/loadstart). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasCompleted) [`XHR.downHasCompleted(xhr) ~> boolean`](#XHR-downHasCompleted) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasCompleted) [`XHR.downHasCompleted(xhr) ~> boolean`](#XHR-downHasCompleted) `XHR.downHasCompleted` returns a possibly observable boolean property that tells whether the download operation of an ongoing XHR has been [completed @@ -410,57 +555,57 @@ successfully](https://developer.mozilla.org/en-US/docs/Web/Events/load). Note that this does not take into account the HTTP response status, see [`XHR.status`](#XHR-status) and [`XHR.isHttpSuccess`](#XHR-isHttpSuccess). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasTimedOut) [`XHR.downHasTimedOut(xhr) ~> boolean`](#XHR-downHasTimedOut) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasTimedOut) [`XHR.downHasTimedOut(xhr) ~> boolean`](#XHR-downHasTimedOut) `XHR.downHasTimedOut` returns a possibly observable boolean property that tells whether the download operation of an ongoing XHR has [timed out](https://developer.mozilla.org/en-US/docs/Web/Events/timeout). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downIsProgressing) [`XHR.downIsProgressing(xhr) ~> boolean`](#XHR-downIsProgressing) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downIsProgressing) [`XHR.downIsProgressing(xhr) ~> boolean`](#XHR-downIsProgressing) `XHR.downIsProgressing` returns a possibly observable boolean property that tells whether the download operation of an ongoing XHR is [progressing](https://developer.mozilla.org/en-US/docs/Web/Events/progress). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downLoaded) [`XHR.downLoaded(xhr) ~> number`](#XHR-downLoaded) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downLoaded) [`XHR.downLoaded(xhr) ~> number`](#XHR-downLoaded) `XHR.downLoaded` returns a possibly observable property of the [`loaded`](https://developer.mozilla.org/en-US/docs/Web/Events/progress) property of an ongoing XHR. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downTotal) [`XHR.downTotal(xhr) ~> number`](#XHR-downTotal) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downTotal) [`XHR.downTotal(xhr) ~> number`](#XHR-downTotal) `XHR.downTotal` returns a possibly observable property of the [`total`](https://developer.mozilla.org/en-US/docs/Web/Events/progress) property of an ongoing XHR. -### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#upload-state) [Upload state](#upload-state) +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#upload-state) [Upload state](#upload-state) -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upError) [`XHR.upError(xhr) ~> exception`](#XHR-upError) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upError) [`XHR.upError(xhr) ~> exception`](#XHR-upError) `XHR.upError` returns a possibly observable property of the [`error`](https://developer.mozilla.org/en-US/docs/Web/Events/error) property of -a failed XHR. +an errored XHR. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasEnded) [`XHR.upHasEnded(xhr) ~> boolean`](#XHR-upHasEnded) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasEnded) [`XHR.upHasEnded(xhr) ~> boolean`](#XHR-upHasEnded) `XHR.upHasEnded` returns a possibly observable boolean property that tells whether the upload operation of an ongoing XHR has [ended](https://developer.mozilla.org/en-US/docs/Web/Events/loadend). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasFailed) [`XHR.upHasFailed(xhr) ~> boolean`](#XHR-upHasFailed) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasErrored) [`XHR.upHasErrored(xhr) ~> boolean`](#XHR-upHasErrored) -`XHR.upHasFailed` returns a possibly observable boolean property that tells +`XHR.upHasErrored` returns a possibly observable boolean property that tells whether the upload operation of an ongoing XHR has -[failed](https://developer.mozilla.org/en-US/docs/Web/Events/error). +[errored](https://developer.mozilla.org/en-US/docs/Web/Events/error). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasStarted) [`XHR.upHasStarted(xhr) ~> boolean`](#XHR-upHasStarted) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasStarted) [`XHR.upHasStarted(xhr) ~> boolean`](#XHR-upHasStarted) `XHR.upHasStarted` returns a possibly observable boolean property that tells whether the upload operation of an ongoing XHR has [started](https://developer.mozilla.org/en-US/docs/Web/Events/loadstart). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasCompleted) [`XHR.upHasCompleted(xhr) ~> boolean`](#XHR-upHasCompleted) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasCompleted) [`XHR.upHasCompleted(xhr) ~> boolean`](#XHR-upHasCompleted) `XHR.upHasCompleted` returns a possibly observable boolean property that tells whether the upload operation of an ongoing XHR has [completed @@ -468,92 +613,30 @@ successfully](https://developer.mozilla.org/en-US/docs/Web/Events/load). Note that this does not take into account the HTTP response status, see [`XHR.status`](#XHR-status) and [`XHR.isHttpSuccess`](#XHR-isHttpSuccess). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasTimedOut) [`XHR.upHasTimedOut(xhr) ~> boolean`](#XHR-upHasTimedOut) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasTimedOut) [`XHR.upHasTimedOut(xhr) ~> boolean`](#XHR-upHasTimedOut) `XHR.upHasTimedOut` returns a possibly observable boolean property that tells whether the upload operation of an ongoing XHR has [timed out](https://developer.mozilla.org/en-US/docs/Web/Events/timeout). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upIsProgressing) [`XHR.upIsProgressing(xhr) ~> boolean`](#XHR-upIsProgressing) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upIsProgressing) [`XHR.upIsProgressing(xhr) ~> boolean`](#XHR-upIsProgressing) `XHR.upIsProgressing` returns a possibly observable boolean property that tells whether the upload operation of an ongoing XHR is [progressing](https://developer.mozilla.org/en-US/docs/Web/Events/progress). -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upLoaded) [`XHR.upLoaded(xhr) ~> number`](#XHR-upLoaded) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upLoaded) [`XHR.upLoaded(xhr) ~> number`](#XHR-upLoaded) `XHR.upLoaded` returns a possibly observable property of the [`loaded`](https://developer.mozilla.org/en-US/docs/Web/Events/progress) property of an ongoing XHR. -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upTotal) [`XHR.upTotal(xhr) ~> number`](#XHR-upTotal) +##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upTotal) [`XHR.upTotal(xhr) ~> number`](#XHR-upTotal) `XHR.upTotal` returns a possibly observable property of the [`total`](https://developer.mozilla.org/en-US/docs/Web/Events/progress) property of an ongoing XHR. -### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#happy-path) [Happy path](#happy-path) - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-result) [`XHR.result(xhr) ~> varies`](#XHR-result) - -`XHR.result` returns the response of a [succeeded](#XHR-hasSucceeded) XHR. Note -that [`XHR.response`](#XHR-response) allows one to obtain the response before -the XHR [is done](#XHR-isDone) and even when the XHR has (partially) failed. - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#happy-path-algebras) [Happy path algebras](#happy-path-algebras) - -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-Succeeded) [`XHR.Succeeded ~> Monad`](#XHR-Succeeded) - -`XHR.Succeeded` is a static land compatible -[monad](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad) -comprised of the [`XHR.ap`](#XHR-ap), [`XHR.chain`](#XHR-chain), -[`XHR.map`](#XHR-map), and [`XHR.of`](#XHR-of) combinators that allows one to -compose sequences of XHR requests that stop as soon as the first XHR does not -[succeed](#XHR-hasSucceeded). - -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-IdentitySucceeded) [`XHR.IdentitySucceeded ~> Monad`](#XHR-IdentitySucceeded) - -`XHR.IdentitySucceeded` is a static land compatible -[monad](https://github.com/rpominov/static-land/blob/master/docs/spec.md#monad) -that manipulates XHRs like [`XHR.Succeeded`](#XHR-Succeeded) or plain data. - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#monadic-happy-path-combinators) [Monadic happy path combinators](#monadic-happy-path-combinators) - -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-ap) [`XHR.ap(xhrAtoB, xhrA) ~> xhrB`](#XHR-ap) - -`XHR.ap` implements a static land compatible -[`ap`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#apply) -function for composing succeeding XHRs. - -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-chain) [`XHR.chain(responseA => xhrB, xhrA) ~> xhrB`](#XHR-chain) - -`XHR.chain` implements a static land compatible -[`chain`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#chain) -function for composing succeeding XHRs. - -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-map) [`XHR.map(responseA => responseB, xhrA) ~> xhrB`](#XHR-map) - -`XHR.map` implements a static land compatible -[`map`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#functor) -function for composing succeeding XHRs. - -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-of) [`XHR.of(response) ~> xhr`](#XHR-of) - -`XHR.of` implements a static land compatible -[`of`](https://github.com/rpominov/static-land/blob/master/docs/spec.md#applicative) -function for composing succeeding XHRs. - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#additional-happy-path-combinators) [Additional happy path combinators](#additional-happy-path-combinators) - -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-apply) [`XHR.apply((...responseAs) => responseB, [...xhrAs]) ~> xhrB`](#XHR-apply) - -`XHR.apply` maps the given XHRs through the given function. - -##### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-template) [`XHR.template([ ... xhr ... ] | { ... xhr ... }) ~> xhr`](#XHR-template) - -`XHR.template` transforms a nested template of plain arrays and objects possibly -containing XHRs into a XHR. - ### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#auxiliary) [Auxiliary](#auxiliary) #### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isHttpSuccess) [`XHR.isHttpSuccess(number) ~> boolean`](#XHR-isHttpSuccess) @@ -563,23 +646,7 @@ numeric property is in the range 2xx of [HTTP success codes](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success). See also [`XHR.statusIsHttpSuccess`](#XHR-statusIsHttpSuccess). -### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#deprecated) [Deprecated](#deprecated) - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-downHasSucceeded) ~~[`XHR.downHasSucceeded(xhr) ~> boolean`](#XHR-downHasSucceeded)~~ - -`XHR.downHasSucceeded` has been renamed to -[`XHR.downHasCompleted`](#XHR-downHasCompleted). - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-headersReceived) ~~[`XHR.headersReceived(xhr) ~> boolean`](#XHR-headersReceived)~~ - -`XHR.headersReceived` has been renamed to -[`XHR.isStatusAvailable`](#XHR-isStatusAvailable). - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-responseFull) ~~[`XHR.responseFull(xhr) ~> varies`](#XHR-responseFull)~~ - -`XHR.responseFull` has been renamed to [`XHR.response`](#XHR-response). - -#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-upHasSucceeded) ~~[`XHR.upHasSucceeded(xhr) ~> boolean`](#XHR-upHasSucceeded)~~ +#### [≡](#contents) [▶](https://calmm-js.github.io/karet.xhr/index.html#XHR-isXHR) [`XHR.isXHR(any) ~> boolean`](#XHR-isXHR) -`XHR.upHasSucceeded` has been renamed to -[`XHR.upHasCompleted`](#XHR-upHasCompleted). +`XHR.isXHR` returns a possibly observable boolean property that tells whether +the given value is a XHR. diff --git a/docs/setup.js b/docs/setup.js index 5ae1bd4..3342afd 100644 --- a/docs/setup.js +++ b/docs/setup.js @@ -1,2 +1,16 @@ window.L = window.Kefir.partial.lenses window.XHR = window.karet.xhr +window.log = function() { + var Kefir = window.Kefir + var log = console.log + var xs = [] + for (let i = 0; i < arguments.length; ++i) { + var x = arguments[i] + xs.push(x instanceof Kefir.Observable ? x : Kefir.constant(x)) + } + var log = console.log + function logs(xs) { + log.apply(console, xs) + } + Kefir.combine(xs).observe(logs, logs) +} diff --git a/package-lock.json b/package-lock.json index f6774ac..2a508e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "karet.xhr", - "version": "0.6.7", + "version": "0.7.0-0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3889,6 +3889,16 @@ "partial.lenses": "^14.10.0" } }, + "kefir.ramda": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/kefir.ramda/-/kefir.ramda-0.25.6.tgz", + "integrity": "sha512-qw5BVPAD2ahZhGNwsc2nOEnhFW6aquX9LZ13Hp1s9cDilhGczluxrfe0pqB+j5y/cSV3AyhapgpOj87SqShQTw==", + "dev": true, + "requires": { + "karet.lift": "^2.1.0", + "ramda": "^0.25.0" + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", diff --git a/package.json b/package.json index 95bbb92..a047495 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "karet.xhr", - "version": "0.6.7", + "version": "0.7.0-0", "description": "An observable wrapper for XMLHttpRequest using Kefir", "module": "dist/karet.xhr.es.js", "main": "dist/karet.xhr.cjs.js", @@ -46,12 +46,12 @@ "eslint": "^5.6.1", "express": "^4.16.3", "kefir": "^3.8.5", + "kefir.ramda": "^0.25.5", "klipse-github-docs-generator": "^0.3.7", "livereload": "^0.7.0", "mocha": "^5.2.0", "mocha-headless-chrome": "^2.0.1", "nyc": "^12.0.1", - "ramda": "^0.25.0", "rollup": "^0.66.4", "rollup-plugin-babel": "^3.0.7", "rollup-plugin-commonjs": "^9.1.8", diff --git a/src/delay-unsub.js b/src/delay-unsub.js new file mode 100644 index 0000000..9b5edbc --- /dev/null +++ b/src/delay-unsub.js @@ -0,0 +1,51 @@ +import * as I from 'infestines' +import * as K from 'kefir' + +const TIMER = 't' +const SOURCE = 's' +const HANDLER = 'h' + +const TYPE = 'type' +const VALUE = 'value' +const END = 'end' + +const DelayUnsub = I.inherit( + function DelayUnsub(source) { + const self = this + K.Property.call(self) + self[SOURCE] = source + self[HANDLER] = self[TIMER] = 0 + }, + K.Property, + { + _onActivation() { + const self = this + if (self[TIMER]) { + clearTimeout(self[TIMER]) + self[TIMER] = 0 + } else { + self[SOURCE].onAny( + (self[HANDLER] = e => { + const t = e[TYPE] + if (t === VALUE) { + self._emitValue(e[VALUE]) + } else if (t === END) { + self._emitEnd() + } else { + self._emitError(e[VALUE]) + } + }) + ) + } + }, + _onDeactivation() { + const self = this + self[TIMER] = setTimeout(() => { + self[SOURCE].offAny(self[HANDLER]) + self[HANDLER] = this[TIMER] = 0 + }, 0) + } + } +) + +export const delayUnsub = source => new DelayUnsub(source) diff --git a/src/karet.xhr.js b/src/karet.xhr.js index 2ba2cf7..742b8b9 100644 --- a/src/karet.xhr.js +++ b/src/karet.xhr.js @@ -6,6 +6,10 @@ import * as V from 'partial.lenses.validation' // +import {delayUnsub} from './delay-unsub' + +// + const isObservable = x => x instanceof K.Observable const toObservable = x => (isObservable(x) ? x : K.constant(x)) @@ -30,13 +34,14 @@ const filter = I.curry(function filter(pr, xs) { } }) -const flatMapLatestToProperty = I.curry(function flatMapLatestToProperty( - fn, - x -) { +const flatMapProperty = I.curry(function flatMapProperty(xyO, x) { return isObservable(x) - ? toProperty(x.flatMapLatest(I.pipe2U(fn, toObservable))) - : toProperty(fn(x)) + ? toProperty(x.flatMapLatest(I.pipe2U(xyO, toObservable))) + : toProperty(xyO(x)) +}) + +const mapProperty = I.curry(function map(xy, x) { + return isObservable(x) ? toProperty(x.map(xy)) : xy(x) }) // @@ -86,7 +91,7 @@ const typeInitial = I.freeze({type: INITIAL}) const typeLoadend = I.freeze({type: LOADEND}) const typeLoad = I.freeze({type: LOAD}) -const eventTypes = [LOADSTART, PROGRESS, TIMEOUT, LOAD, ERROR] +const eventTypes = [LOADSTART, PROGRESS, LOAD, TIMEOUT, ERROR] const hasKeys = x => I.isFunction(x.keys) @@ -120,57 +125,59 @@ const performPlain = (process.env.NODE_ENV === 'production' V.accept ) ))(function perform(args) { - return K.stream(({emit, end}) => { - const url = args.url - const method = args.method - const user = args.user - const password = args.password - const headers = args.headers - const overrideMimeType = args[OVERRIDE_MIME_TYPE] - const body = args.body - const responseType = args[RESPONSE_TYPE] - const timeout = args[TIMEOUT] - const withCredentials = args[WITH_CREDENTIALS] - - const xhr = new XMLHttpRequest() - let state = {xhr, up: typeInitial, down: typeInitial, map: I.id} - const update = (dir, type) => event => { - if (type !== PROGRESS || state[dir].type !== LOAD) - emit((state = L.set(dir, {type, event}, state))) - } - eventTypes.forEach(type => { - xhr[ADD_EVENT_LISTENER](type, update(DOWN, type)) - xhr.upload[ADD_EVENT_LISTENER](type, update(UP, type)) - }) - xhr[ADD_EVENT_LISTENER](READYSTATECHANGE, event => { - emit((state = L.set(EVENT, event, state))) - }) - xhr[ADD_EVENT_LISTENER](LOADEND, event => { - end(emit((state = L.set(EVENT, event, state)))) + return delayUnsub( + K.stream(({emit, end}) => { + const url = args.url + const method = args.method + const user = args.user + const password = args.password + const headers = args.headers + const overrideMimeType = args[OVERRIDE_MIME_TYPE] + const body = args.body + const responseType = args[RESPONSE_TYPE] + const timeout = args[TIMEOUT] + const withCredentials = args[WITH_CREDENTIALS] + + const xhr = new XMLHttpRequest() + let state = {xhr, up: typeInitial, down: typeInitial, map: I.id} + const update = (dir, type) => event => { + if (type !== PROGRESS || state[dir].type !== LOAD) + emit((state = L.set(dir, {type, event}, state))) + } + eventTypes.forEach(type => { + xhr[ADD_EVENT_LISTENER](type, update(DOWN, type)) + xhr.upload[ADD_EVENT_LISTENER](type, update(UP, type)) + }) + xhr[ADD_EVENT_LISTENER](READYSTATECHANGE, event => { + emit((state = L.set(EVENT, event, state))) + }) + xhr[ADD_EVENT_LISTENER](LOADEND, event => { + end(emit((state = L.set(EVENT, event, state)))) + }) + xhr.open( + isNil(method) ? 'GET' : method, + url, + true, + isNil(user) ? null : user, + isNil(password) ? null : password + ) + if (responseType) { + xhr[RESPONSE_TYPE] = responseType + if (responseType === JSON_ && xhr[RESPONSE_TYPE] !== JSON_) + state = L.set(MAP, tryParse, state) + } + if (timeout) xhr[TIMEOUT] = timeout + if (withCredentials) xhr[WITH_CREDENTIALS] = withCredentials + for (const header in headers) { + xhr.setRequestHeader(header, headers[header]) + } + if (overrideMimeType) xhr[OVERRIDE_MIME_TYPE](overrideMimeType) + xhr.send(isNil(body) ? null : body) + return () => { + if (!xhr[STATUS]) xhr.abort() + } }) - xhr.open( - isNil(method) ? 'GET' : method, - url, - true, - isNil(user) ? null : user, - isNil(password) ? null : password - ) - if (responseType) { - xhr[RESPONSE_TYPE] = responseType - if (responseType === JSON_ && xhr[RESPONSE_TYPE] !== JSON_) - state = L.set(MAP, tryParse, state) - } - if (timeout) xhr[TIMEOUT] = timeout - if (withCredentials) xhr[WITH_CREDENTIALS] = withCredentials - for (const header in headers) { - xhr.setRequestHeader(header, headers[header]) - } - if (overrideMimeType) xhr[OVERRIDE_MIME_TYPE](overrideMimeType) - xhr.send(isNil(body) ? null : body) - return () => { - if (!xhr[STATUS]) xhr.abort() - } - }) + ) }) const toLowerKeyedObject = L.get([ @@ -220,7 +227,7 @@ const normalizeOptions = (process.env.NODE_ENV === 'production' ) export const perform = setName( - I.pipe2U(normalizeOptions, flatMapLatestToProperty(performPlain)), + I.pipe2U(normalizeOptions, flatMapProperty(performPlain)), 'perform' ) @@ -238,10 +245,12 @@ const hasStartedOn = is(eventTypes) const isProgressingOn = is([PROGRESS, LOADSTART]) const load = [LOAD] const hasCompletedOn = is(load) -const hasFailedOn = is([ERROR]) +const hasErroredOn = is([ERROR]) const hasTimedOutOn = is([TIMEOUT]) -const ended = [LOAD, ERROR, TIMEOUT] +const ended = [LOAD, TIMEOUT, ERROR] const hasEndedOn = is(ended) +const failed = [ERROR, TIMEOUT] +const hasFailedOn = is(failed) const event = I.curry((prop, op, dir) => op([dir, EVENT, prop])) const loadedOn = event(LOADED, L.sum) @@ -261,7 +270,8 @@ const either = L.branches(DOWN, UP) export const upHasStarted = setName(hasStartedOn(UP), 'upHasStarted') export const upIsProgressing = setName(isProgressingOn(UP), 'upIsProgressing') export const upHasCompleted = setName(hasCompletedOn(UP), 'upHasCompleted') -export const upHasFailed = setName(hasFailedOn(UP), 'upHasFailed') +export const upHasFailed = setName(hasErroredOn(UP), 'upHasFailed') +export const upHasErrored = setName(hasErroredOn(UP), 'upHasErrored') export const upHasTimedOut = setName(hasTimedOutOn(UP), 'upHasTimedOut') export const upHasEnded = setName(hasEndedOn(UP), 'upHasEnded') export const upLoaded = setName(loadedOn(UP), 'upLoaded') @@ -278,6 +288,7 @@ export const downHasCompleted = setName( 'downHasCompleted' ) export const downHasFailed = setName(hasFailedOn(DOWN), 'downHasFailed') +export const downHasErrored = setName(hasErroredOn(DOWN), 'downHasErrored') export const downHasTimedOut = setName(hasTimedOutOn(DOWN), 'downHasTimedOut') export const downHasEnded = setName(hasEndedOn(DOWN), 'downHasEnded') export const downLoaded = setName(loadedOn(DOWN), 'downLoaded') @@ -291,7 +302,7 @@ export const isStatusAvailable = setName( ) export const isDone = setName(is([LOADEND], EVENT), 'isDone') export const isProgressing = setName(isProgressingOn(either), 'isProgressing') -export const hasFailed = setName(hasFailedOn(either), 'hasFailed') +export const hasErrored = setName(hasErroredOn(either), 'hasErrored') export const hasTimedOut = setName(hasTimedOutOn(either), 'hasTimedOut') export const loaded = setName(loadedOn(either), 'loaded') export const total = setName(totalOn(either), 'total') @@ -399,13 +410,21 @@ export const hasSucceeded = setName( 'hasSucceeded' ) +export const hasFailed = F.lift(function hasFailed(xhr) { + return ( + (isStatusAvailable(xhr) && !statusIsHttpSuccess(xhr)) || + downHasFailed(xhr) || + upHasFailed(xhr) + ) +}) + export const getJson = setName( I.pipe2U( performJson, - flatMapLatestToProperty(xhr => { + flatMapProperty(xhr => { if (hasSucceeded(xhr)) { return response(xhr) - } else if (isDone(xhr) && (hasFailed(xhr) || hasTimedOut(xhr))) { + } else if (isDone(xhr) && (hasErrored(xhr) || hasTimedOut(xhr))) { return K.constantError(xhr) } else { return never @@ -420,30 +439,30 @@ export const result = setName(getAfter(hasSucceeded, response), 'result') const getAllResponseHeaders = I.always('') const getResponseHeader = I.always(null) -export const of = response => ({ - event: typeLoadend, - up: typeInitial, - down: typeLoad, - xhr: { - getAllResponseHeaders, - getResponseHeader, - readyState: 4, - response, - status: 200, - statusText: 'OK' - }, - map: I.id +export const of = F.lift(function of(response) { + const source = arguments.length > 1 && arguments[1] + return { + event: typeLoadend, + up: source ? source[UP] : typeInitial, + down: source ? source[DOWN] : typeLoad, + xhr: { + getAllResponseHeaders, + getResponseHeader, + readyState: 4, + response, + status: 200, + statusText: 'OK' + }, + map: I.id + } }) export const chain = I.curry(function chain(fn, xhr) { - return flatMapLatestToProperty( - x => (hasSucceeded(x) ? fn(response(x)) : x), - xhr - ) + return flatMapProperty(x => (hasSucceeded(x) ? fn(response(x)) : x), xhr) }) export const map = I.curry(function map(fn, xhr) { - return chain(I.pipe2U(fn, of), xhr) + return chain(response => of(fn(response), xhr), xhr) }) export const ap = I.curry(function ap(f, x) { @@ -452,6 +471,53 @@ export const ap = I.curry(function ap(f, x) { export const Succeeded = I.Monad(map, of, ap, chain) +const readyState1 = I.freeze({readyState: 1}) + +// + +const mergeTypes = (dir, lhs, rhs) => { + const lhsIndex = eventTypes.indexOf(lhs) + const rhsIndex = eventTypes.indexOf(rhs) + return ((rhsIndex - lhsIndex + dir) ^ dir) < 0 ? lhs : rhs +} + +const mergeEvents = (dir, lhs, rhs) => ({ + type: mergeTypes(dir, lhs[TYPE], rhs[TYPE]), + event: { + total: (L.get(TOTAL, lhs[EVENT]) || 0) + (L.get(TOTAL, rhs[EVENT]) || 0), + loaded: (L.get(LOADED, lhs[EVENT]) || 0) + (L.get(LOADED, rhs[EVENT]) || 0) + } +}) + +const mergeXHRs = (dir, lhs, rhs) => ({ + xhr: readyState1, + up: mergeEvents(dir, lhs[UP], rhs[UP]), + down: mergeEvents(dir, lhs[DOWN], rhs[DOWN]), + map: I.id +}) + +export const apParallel = I.curry(function apParallel(f, x) { + return flatMapProperty( + f => + hasFailed(f) + ? f + : mapProperty( + x => + hasSucceeded(f) + ? hasSucceeded(x) + ? of(result(f)(result(x)), mergeXHRs(0, f, x)) + : hasFailed(x) + ? x + : mergeXHRs(-1, f, x) + : mergeXHRs(-1, f, x), + x + ), + f + ) +}) + +export const Parallel = I.Applicative(map, of, apParallel) + const typeIsString = [TYPE, I.isString] export const isXHR = setName( @@ -467,10 +533,11 @@ export const isXHR = setName( ) export const IdentitySucceeded = I.IdentityOrU(isXHR, Succeeded) +export const IdentityParallel = I.IdentityOrU(isXHR, Parallel) export const template = setName( L.get([ - L.traverse(IdentitySucceeded, I.id, F.inTemplate(isXHR)), + L.traverse(IdentityParallel, I.id, F.inTemplate(isXHR)), L.ifElse(isXHR, [], of) ]), 'template' @@ -479,24 +546,3 @@ export const template = setName( export const apply = I.curry(function apply(f, xs) { return map(xs => f.apply(null, xs), template(xs)) }) - -const renamed = - process.env.NODE_ENV === 'production' - ? x => x - : function renamed(fn, name) { - let warned = false - return setName(function deprecated(x) { - if (!warned) { - warned = true - console.warn( - 'karet.xhr: `' + name + '` has been renamed to `' + fn.name + '`' - ) - } - return fn(x) - }, name) - } - -export const downHasSucceeded = renamed(downHasCompleted, 'downHasSucceeded') -export const headersReceived = renamed(isStatusAvailable, 'headersReceived') -export const responseFull = renamed(response, 'responseFull') -export const upHasSucceeded = renamed(upHasCompleted, 'upHasSucceeded') diff --git a/test/tests.js b/test/tests.js index 99328d6..f60de93 100644 --- a/test/tests.js +++ b/test/tests.js @@ -1,6 +1,6 @@ import * as I from 'infestines' import * as K from 'kefir' -import * as R from 'ramda' +import * as R from 'kefir.ramda' import * as XHR from './generated/dist/karet.xhr.es.js' @@ -155,7 +155,7 @@ describe('XHR', () => { ) ) testEq([false], () => - XHR.hasFailed( + XHR.hasErrored( startingWithNonXHRs(XHR.performJson({url: 'http://localhost:3000/json'})) ) ) @@ -257,25 +257,29 @@ describe('XHR', () => { testEq( [ [ - {simonSays: 'Hello, world!'}, - ['constant'], - ['message from', {user: 'world'}] + 29, + [ + {simonSays: 'Hello, world!'}, + ['constant'], + ['message from', {user: 'world'}] + ] ] ], - () => - XHR.result( - XHR.apply((x, y, z) => [x, y, z], [ - {simonSays: XHR.perform('http://localhost:3000/text')}, - [K.constant('constant')], - ['message from', XHR.performJson('http://localhost:3000/json')] - ]) - ) + () => { + const xhr = XHR.apply((x, y, z) => [x, y, z], [ + {simonSays: XHR.perform('http://localhost:3000/text')}, + [K.constant('constant')], + ['message from', XHR.performJson('http://localhost:3000/json')] + ]) + return R.pair(XHR.total(xhr), XHR.result(xhr)) + } ) testEq(true, () => XHR.isXHR(XHR.template([1, 2, {x: 3}]))) testEq( { allResponseHeaders: '', errors: [], + hasErrored: false, hasFailed: false, hasTimedOut: false, isDone: true, @@ -292,6 +296,7 @@ describe('XHR', () => { R.map(fn => fn(XHR.of(101)), { allResponseHeaders: XHR.allResponseHeaders, errors: XHR.errors, + hasErrored: XHR.hasErrored, hasFailed: XHR.hasFailed, hasTimedOut: XHR.hasTimedOut, isDone: XHR.isDone, @@ -307,12 +312,6 @@ describe('XHR', () => { ) }) -describe('XHR deprecated', () => { - testEq([false, true], () => - XHR.downHasSucceeded(XHR.perform('http://localhost:3000/json')) - ) -}) - describe('IE11 workarounds', () => { let progressEvent = null let progressAction = null