[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\"es2015\"],\n  \"plugins\": [\"transform-object-rest-spread\"]\n}\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  extends: ['unobtrusive', 'prettier'],\n  plugins: ['prettier'],\n  parser: 'babel-eslint',\n  env: {\n    browser: true,\n    jest: true,\n    es6: true,\n  },\n  rules: {\n    'prettier/prettier': 'error',\n  },\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "lib/\n"
  },
  {
    "path": ".npmignore",
    "content": ""
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - \"8\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "### 0.2.1 - 5th Sept 2016\n- ensure `onChange` is kept when a new instance is created\n\n### 0.2.0 - 5th Sept 2016\n- `case` statement support\n- tests refactored to use Jest\n\n### 0.1.0 - 11th June 2016\n- Initial \"beta\" release\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Jack Franklin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# RemoteData.js\n\nInspired by Kris Jenkins'\n[RemoteData](http://package.elm-lang.org/packages/krisajenkins/elm-exts/25.1.0/Exts-RemoteData)\nElm package, this library provides an object for representing remote data in\nyour application.\n\n[![Build Status](https://travis-ci.org/jackfranklin/remote-data-js.svg?branch=master)](https://travis-ci.org/jackfranklin/remote-data-js)\n\n```\nnpm install --save remote-data-js\n```\n\n## Motivations\n\nBy representing the data and the state of the request in one object it becomes\nimpossible for you to have data that's out of sync.\n\nA typical app might model the data as:\n\n```\n{ loading: true, data: undefined }\n```\n\nAnd then update the values when the request succeeds. However, this really is\none piece of information that is now represented across two keys, and as such it\ncan become out of sync.\n\nInstead, `RemoteData` models both the _request_ and the _data_ in one object, so\nthey can never be out of sync with each other.\n\nA `RemoteData` instance has one of four states:\n\n* `NOT_ASKED` - you've got started the request yet\n* `PENDING` - the request is in flight\n* `SUCCESS` - we have data from the request\n* `FAILURE` - the request went wrong, we have an error for it\n\nYou can check the status of a `RemoteData` instance and therefore represent data\nin your application accordingly.\n\nAdditionally, `RemoteData` instances are _never_ mutated, but pass a new version\nof themselves through callbacks. This means any mutation bugs with rendering off\nyour remote data instances are not a concern, and that this library can play\nnicely with React, Redux and others.\n\n## Example\n\n```js\nimport RemoteData from 'remote-data'\n\nconst githubPerson = new RemoteData({\n  url: username => `https://api.github.com/users/${username}`,\n  onChange: remoteData => console.log('State changed!', remoteData),\n})\n\n// then later on\n\ngithubPerson\n  .fetch('jackfranklin')\n  .then(remoteData => {\n    // it worked fine\n    console.log(remoteData.isSuccess()) // true\n    console.log(remoteData.data) // github api data\n    console.log(remoteData.response.status) // 200\n  })\n  .catch(remoteData => {\n    // something went wrong\n    console.log(remoteData.isSuccess()) // false\n    console.log(remoteData.isFailure()) // true\n    console.log(remoteData.data) // error info\n    console.log(remoteData.response.status) // response status code\n  })\n```\n\n## API\n\n### Creating `RemoteData` instances\n\nThe configuration you can provide when creating a new instance of RemoteData are\nas follows:\n\n```js\nconst instance = new RemoteData({\n  url: (name) => `https://api.github.com/users/${username}`\n  onChange: (newInstance) => {...},\n  parse: (response) => response.json,\n  fetchOptions: {}\n});\n```\n\nThese are fully documented below:\n\n* `url: String | Function`: if given a string, this will be the URL that the\n  request is made to. If it's a function it will be called when `fetch` is\n  called, passing any arguments through. For example, `remoteData.fetch('jack')`\n  will call the `url` function, passing `jack` as the argument.\n\n* `onChange: Function`: a function called whenever the state of a remote data\n  instance changes. This is passed in the new RemoteData instance.\n\n* `parse: Function`: a function used to parse the `Response` from the HTTP\n  request. Defaults to `response.json()`.\n\n* `fetchOptions: Object`: an object that is passed through to `fetch` and allows\n  you to configure headers and any other request options.\n\n### Making Requests\n\nTo make a request, call `fetch` on the `RemoteData` instance:\n\n```js\nconst githubPerson = new RemoteData({\n  url: name => `https://api.github.com/users/${username}`,\n  onChange: newGithubPerson => console.log(newGithubPerson),\n})\n\ngithubPerson.fetch('jackfranklin')\n```\n\nA promise is returned and the value it will resolve to is the new `RemoteData`\ninstance:\n\n```js\ngithubPerson.fetch('jackfranklin').then(newData => {\n  console.log(newData.data) // GitHub API data, parsed from JSON\n  console.log(newData.response.status) // status code\n  console.log(newData.state) // 'SUCCESS'\n})\n```\n\n### Checking the status of a request\n\nYou can call any of the following methods:\n\n* `isFinished()` : true if a request has succeeded or failed.\n* `isNotAsked()` : true if the request hasn't been asked for (this is the\n  default state).\n* `isPending()` : true if the request has been started but is pending\n* `isFailure()` : true if the request has failed\n* `isSuccess()` : true if the request has succeeded\n\nYou can \"switch\" on a RemoteData instance's state similarly to functional\nlanguages and the JavaScript\n[Union Type](https://www.npmjs.com/package/union-type) package:\n\n```js\ngithubPerson.fetch('jackfranklin').then(data => {\n  const message = data.case({\n    NotAsked: () => 'Initializing...',\n    Pending: () => 'Loading...',\n    Success: data => renderData(data),\n    Failure: error => renderError(error),\n  })\n})\n```\n\nIf you don't handle all four possible states, you must include a default handler\nnamed `_` (underscore):\n\n```js\ngithubPerson.fetch('jackfranklin').then(data => {\n  const message = data.case({\n    Success: data => renderData(data),\n    Failure: error => renderError(error),\n    _: () => 'Loading...',\n  })\n})\n```\n\nYou can call `.data` on a request to access the data, but be aware that this\n_will throw an error_ if the request hasn't been asked for or is pending.\n\nYou can call `.response` on a request to access the response, but be aware that\nthis _will throw an error_ if the request hasn't been asked for or is pending.\n\n## Making remote data instances from a promise.\n\nLet's say you have your own custom API library in your app for making API\nrequests that returns promises. In this instance, you don't want to use\nRemoteData's own `fetch` based API to initiate the request, but instead you want\nto wrap your promise in a `RemoteData` instance:\n\n```js\nimport { fromPromise } from 'remote-data-js'\nimport myCustomApiRequestLib from 'my-custom-lib'\n\nconst onChange = newRemoteData => {...}\n\nconst apiRequest = myCustomApiRequestLib('/foo')\nconst remoteDataInstance = fromPromise(apiRequest, { onChange })\nremoteDataInstance.isPending() // => true\n```\n"
  },
  {
    "path": "__tests__/from-promise-test.js",
    "content": "import { fromPromise } from '../src/index'\n\nit('can be constructed from a promise and be in the loading state', () => {\n  const onChange = () => {}\n  const prom = new Promise(resolve => resolve({ success: true }))\n  const remoteData = fromPromise(prom, { onChange })\n  expect(remoteData.isPending()).toBe(true)\n})\n\nit('gives the data back when it succeeds', done => {\n  const onChange = res => {\n    expect(res.isSuccess()).toEqual(true)\n    expect(res.data).toEqual({ success: true })\n    done()\n  }\n  const prom = new Promise(resolve => resolve({ success: true }))\n  const remoteData = fromPromise(prom, { onChange })\n  expect(remoteData.isPending()).toBe(true)\n})\n\nit('gives the error back when it fails', done => {\n  const onChange = res => {\n    expect(res.isFailure()).toEqual(true)\n    expect(res.data).toEqual({ error: true })\n    done()\n  }\n  /* eslint-disable prefer-promise-reject-errors */\n  const prom = new Promise((resolve, reject) => reject({ error: true }))\n  /* eslint-enable prefer-promise-reject-errors */\n  fromPromise(prom, { onChange })\n})\n"
  },
  {
    "path": "__tests__/remote-data-test.js",
    "content": "import RemoteData from '../src/index'\nimport fetchMock from 'fetch-mock'\n\nconst mockSuccess = url => fetchMock.mock(url, { success: true })\nconst mockError = url => fetchMock.mock(url, 404)\n\nconst resetAndMockSuccess = url => {\n  fetchMock.restore()\n  mockSuccess(url)\n}\n\nconst resetAndMockError = url => {\n  fetchMock.restore()\n  mockError(url)\n}\n\nconst resetAndMockWithResponse = (url, response) => {\n  fetchMock.restore()\n  fetchMock.mock(url, response)\n}\n\nconst makeInstance = (obj = {}) => {\n  const args = Object.assign(\n    {},\n    {\n      url: 'api.com/1',\n    },\n    obj\n  )\n\n  return new RemoteData(args)\n}\n\nit('Is NOT_ASKED by default', () => {\n  expect(new RemoteData().isNotAsked()).toBe(true)\n})\n\ndescribe('defining the URL', () => {\n  it('uses a url if it is given one', () => {\n    resetAndMockSuccess('api.com/1')\n\n    const instance = makeInstance({\n      url: x => `api.com/${x}`,\n    })\n\n    return instance.fetch('1').then(() => {\n      expect(fetchMock.called('api.com/1')).toBe(true)\n    })\n  })\n\n  it('uses a string if given a string', () => {\n    resetAndMockSuccess('api.com/1')\n    const instance = makeInstance()\n\n    return instance.fetch('1').then(() => {\n      expect(fetchMock.called('api.com/1')).toBe(true)\n    })\n  })\n})\n\nit('calls onChange twice when there is a successful request', () => {\n  resetAndMockSuccess('api.com/1')\n  const onChange = jest.fn()\n  const instance = makeInstance({ onChange })\n\n  return instance.fetch().then(() => {\n    expect(onChange.mock.calls[0][0].isPending()).toBe(true)\n    expect(onChange.mock.calls[1][0].isSuccess()).toBe(true)\n  })\n})\n\nit('calls onChange twice for a failed request', () => {\n  resetAndMockError('api.com/1')\n  const onChange = jest.fn()\n  const instance = makeInstance({ onChange })\n\n  return instance.fetch().then(() => {\n    expect(onChange.mock.calls[0][0].isPending()).toBe(true)\n    expect(onChange.mock.calls[1][0].isFailure()).toBe(true)\n  })\n})\n\nit('parses as JSON by default', () => {\n  resetAndMockWithResponse('api.com/1', { some: 'json' })\n  const instance = makeInstance()\n\n  return instance.fetch().then(result => {\n    expect(result.data).toEqual({ some: 'json' })\n  })\n})\n\nit('allows a custom parser function to be provided', () => {\n  resetAndMockWithResponse('api.com/1', 'Hello World')\n  const instance = makeInstance({ parse: x => x.text() })\n\n  return instance.fetch().then(result => {\n    expect(result.data).toEqual('Hello World')\n  })\n})\n\nit('provides `response` to allow access to the raw HTTP response', () => {\n  resetAndMockWithResponse('api.com/1', { some: 'json' })\n  const instance = makeInstance()\n\n  return instance.fetch().then(result => {\n    expect(result.data).toEqual({ some: 'json' })\n    expect(result.response.status).toEqual(200)\n  })\n})\n\nit('throws if you access the response when the request is not finished', () => {\n  const instance = makeInstance()\n\n  expect(() => instance.response).toThrowError(\n    /Cannot get response for request that hasn't finished/\n  )\n})\n\nit('throws if you try to access the data before it has been fetched', () => {\n  const instance = makeInstance()\n\n  expect(() => instance.data).toThrowError(\n    /Cannot get data for request that hasn't finished/\n  )\n})\n\ndescribe('the case method', () => {\n  it('first calls the NotAsked callback', () => {\n    const instance = makeInstance()\n\n    const notAsked = jest.fn()\n    const otherFn = jest.fn()\n\n    instance.case({\n      NotAsked: notAsked,\n      Pending: otherFn,\n      Failure: otherFn,\n      Success: otherFn,\n    })\n\n    expect(notAsked).toBeCalled()\n    expect(otherFn).not.toBeCalled()\n  })\n\n  it('calls the success callback when data has been fetched', () => {\n    resetAndMockSuccess('api.com/1')\n    const instance = makeInstance()\n\n    const successFn = jest.fn()\n    const otherFn = jest.fn()\n\n    return instance.fetch().then(result => {\n      result.case({\n        NotAsked: otherFn,\n        Pending: otherFn,\n        Failure: otherFn,\n        Success: successFn,\n      })\n\n      expect(successFn).toBeCalledWith({ success: true })\n      expect(otherFn).not.toBeCalled()\n    })\n  })\n\n  it('calls the pending callback when the request is in motion', () => {\n    resetAndMockSuccess('api.com/1')\n\n    const pendingFn = jest.fn()\n    const otherFn = jest.fn()\n\n    let count = 0\n    const instance = new RemoteData({\n      url: 'api.com/1',\n      onChange(remoteData) {\n        if (count++ > 0) return\n\n        remoteData.case({\n          NotAsked: otherFn,\n          Pending: pendingFn,\n          Failure: otherFn,\n          Success: otherFn,\n        })\n\n        expect(pendingFn).toBeCalled()\n        expect(otherFn).not.toBeCalled()\n      },\n    })\n\n    return instance.fetch()\n  })\n\n  it('calls the failure callback when the request failed', () => {\n    resetAndMockError('api.com/1')\n\n    const instance = makeInstance()\n\n    const failedFn = jest.fn()\n    const otherFn = jest.fn()\n\n    return instance.fetch().then(result => {\n      result.case({\n        NotAsked: otherFn,\n        Pending: otherFn,\n        Failure: failedFn,\n        Success: otherFn,\n      })\n\n      expect(failedFn).toBeCalled()\n      expect(otherFn).not.toBeCalled()\n    })\n  })\n\n  it('will call the default callback if none match', () => {\n    resetAndMockError('api.com/1')\n    const handler = jest.fn()\n    const instance = makeInstance()\n\n    instance.case({\n      _: handler,\n    })\n\n    expect(handler).toBeCalled()\n  })\n\n  it('copies the onChange event over when a new remote data instance is created', () => {\n    const onChange = jest.fn()\n    const instance = makeInstance({\n      onChange,\n    })\n\n    resetAndMockSuccess('api.com/1')\n\n    return instance.fetch().then(newInstance => {\n      expect(onChange.mock.calls.length).toBe(2)\n      expect(newInstance.onChange).toBe(onChange)\n    })\n  })\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"remote-data-js\",\n  \"version\": \"0.2.1\",\n  \"description\": \"\",\n  \"main\": \"lib/index.js\",\n  \"scripts\": {\n    \"build-cjs\": \"babel src -d lib\",\n    \"prepack\": \"npm run build-cjs\",\n    \"jest\": \"jest\",\n    \"test\": \"npm run lint && npm run jest\",\n    \"watch\": \"npm run jest --watch\",\n    \"lint\": \"eslint src/*.js __tests__/*.js\",\n    \"lint-fix\": \"npm run lint -- --fix\"\n  },\n  \"keywords\": [],\n  \"author\": \"Jack Franklin\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"babel-cli\": \"6.9.0\",\n    \"babel-core\": \"^6.6.0\",\n    \"babel-eslint\": \"^8.0.3\",\n    \"babel-jest\": \"^22.0.1\",\n    \"babel-plugin-transform-object-rest-spread\": \"6.8.0\",\n    \"babel-polyfill\": \"6.13.0\",\n    \"babel-preset-es2015\": \"^6.6.0\",\n    \"babel-register\": \"6.9.0\",\n    \"eslint\": \"^4.11.0\",\n    \"eslint-config-prettier\": \"^2.7.0\",\n    \"eslint-config-standard\": \"^10.2.1\",\n    \"eslint-config-unobtrusive\": \"^1.2.1\",\n    \"eslint-plugin-import\": \"^2.8.0\",\n    \"eslint-plugin-node\": \"^5.2.1\",\n    \"eslint-plugin-prettier\": \"^2.4.0\",\n    \"eslint-plugin-promise\": \"^3.6.0\",\n    \"eslint-plugin-standard\": \"^3.0.1\",\n    \"fetch-mock\": \"4.5.4\",\n    \"jest\": \"^21.2.1\",\n    \"prettier\": \"^1.8.2\",\n    \"webpack-notifier\": \"1.3.0\"\n  }\n}\n"
  },
  {
    "path": "prettier.config.js",
    "content": "module.exports = {\n  semi: false,\n  trailingComma: 'es5',\n  singleQuote: true,\n  proseWrap: 'always',\n}\n"
  },
  {
    "path": "src/index.js",
    "content": "import { NOT_ASKED, PENDING, FAILURE, SUCCESS } from './states'\n\nimport { makeFetchRequest } from './request'\n\nclass RemoteData {\n  constructor({\n    url,\n    state = NOT_ASKED,\n    onChange = () => {},\n    parse = x => x.json(),\n    fetchOptions = {},\n    rawResponse,\n    stateData,\n  } = {}) {\n    this.state = state\n    this.url = url\n    this.onChange = onChange\n    this.parse = parse\n    this.fetchOptions = fetchOptions\n    this.stateData = stateData\n    this.rawResponse = rawResponse\n  }\n\n  config() {\n    const keys = [\n      'onSuccess',\n      'onFailure',\n      'parse',\n      'fetchOptions',\n      'state',\n      'url',\n      'onChange',\n    ]\n    const config = {}\n    keys.forEach(k => {\n      config[k] = this[k]\n    })\n    return config\n  }\n\n  // the default implementations here call through to the _ function\n  // which is called if the user does not provide a function\n  /* eslint-disable no-use-before-define */\n  case({\n    NotAsked = () => _(),\n    Pending = () => _(),\n    Failure = (...args) => _(...args),\n    Success = (...args) => _(...args),\n    _ = () => {},\n  }) {\n    /* eslint-enable no-use-before-define */\n    switch (this.state) {\n      case NOT_ASKED:\n        return NotAsked()\n      case PENDING:\n        return Pending()\n      case FAILURE:\n        return Failure(this.stateData)\n      case SUCCESS:\n        return Success(this.stateData)\n    }\n  }\n\n  isFinished() {\n    return this.isFailure() || this.isSuccess()\n  }\n\n  isPending() {\n    return this.state === PENDING\n  }\n\n  isNotAsked() {\n    return this.state === NOT_ASKED\n  }\n\n  isFailure() {\n    return this.state === FAILURE\n  }\n\n  isSuccess() {\n    return this.state === SUCCESS\n  }\n\n  get data() {\n    if (this.isFinished()) {\n      return this.stateData\n    } else {\n      throw new Error(\"Cannot get data for request that hasn't finished\")\n    }\n  }\n\n  get response() {\n    if (this.isFinished()) {\n      return this.rawResponse\n    } else {\n      throw new Error(\"Cannot get response for request that hasn't finished\")\n    }\n  }\n\n  makeNewAndOnChange(opts = {}) {\n    const newRemoteData = new this.constructor({\n      ...this.config(),\n      ...opts,\n    })\n\n    this.onChange(newRemoteData)\n    return newRemoteData\n  }\n\n  fetch(...args) {\n    const reqUrl = typeof this.url === 'function' ? this.url(...args) : this.url\n    this.makeNewAndOnChange({ state: PENDING })\n\n    return makeFetchRequest(this, reqUrl)\n  }\n}\n\nexport const fromPromise = (promise, { onChange = () => {} } = {}) => {\n  const instance = new RemoteData({ onChange, state: PENDING })\n\n  promise.then(\n    result => {\n      return instance.makeNewAndOnChange({\n        state: SUCCESS,\n        stateData: result,\n      })\n    },\n    error => {\n      return instance.makeNewAndOnChange({\n        state: FAILURE,\n        stateData: error,\n      })\n    }\n  )\n\n  return instance\n}\n\nexport default RemoteData\n"
  },
  {
    "path": "src/request.js",
    "content": "import { FAILURE, SUCCESS } from './states'\n\nconst checkStatus = response => {\n  if (response.status >= 200 && response.status < 300) {\n    return response\n  } else {\n    const error = new Error(response.statusText)\n    error.response = response\n    throw error\n  }\n}\n\nconst dataResponse = (remoteDataInstance, rawResponse, data, isError) => {\n  return remoteDataInstance.makeNewAndOnChange({\n    state: isError ? FAILURE : SUCCESS,\n    stateData: data,\n    rawResponse,\n  })\n}\n\nconst successfulResponse = (remoteDataInstance, rawResponse, data) => {\n  return dataResponse(remoteDataInstance, rawResponse, data, false)\n}\n\nconst failureResponse = (remoteDataInstance, rawResponse, data) => {\n  return dataResponse(remoteDataInstance, rawResponse, data, true)\n}\n\nconst parseAndKeepResponse = parseFn => result => {\n  return Promise.all([\n    Promise.resolve(result),\n    Promise.resolve(parseFn(result)),\n  ])\n}\n\nexport const makeFetchRequest = (remoteDataInstance, url) => {\n  return fetch(url, remoteDataInstance.fetchOptions)\n    .then(checkStatus)\n    .then(parseAndKeepResponse(remoteDataInstance.parse))\n    .then(([rawResponse, data]) =>\n      successfulResponse(remoteDataInstance, rawResponse, data)\n    )\n    .catch(error => failureResponse(remoteDataInstance, error.response, error))\n}\n"
  },
  {
    "path": "src/states.js",
    "content": "export const NOT_ASKED = 'REMOTE_DATA_NOT_ASKED'\nexport const PENDING = 'REMOTE_DATA_PENDING'\nexport const FAILURE = 'REMOTE_DATA_FAILURE'\nexport const SUCCESS = 'REMOTE_DATA_SUCCESS'\n"
  }
]