{"action":"created","issue":{"url":"https://api.github.com/repos/redux-observable/redux-observable/issues/76","repository_url":"https://api.github.com/repos/redux-observable/redux-observable","labels_url":"https://api.github.com/repos/redux-observable/redux-observable/issues/76/labels{/name}","comments_url":"https://api.github.com/repos/redux-observable/redux-observable/issues/76/comments","events_url":"https://api.github.com/repos/redux-observable/redux-observable/issues/76/events","html_url":"https://github.com/redux-observable/redux-observable/issues/76","id":165936100,"number":76,"title":"Effects as data","user":{"login":"shuhei","id":498635,"avatar_url":"https://avatars.githubusercontent.com/u/498635?v=3","gravatar_id":"","url":"https://api.github.com/users/shuhei","html_url":"https://github.com/shuhei","followers_url":"https://api.github.com/users/shuhei/followers","following_url":"https://api.github.com/users/shuhei/following{/other_user}","gists_url":"https://api.github.com/users/shuhei/gists{/gist_id}","starred_url":"https://api.github.com/users/shuhei/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/shuhei/subscriptions","organizations_url":"https://api.github.com/users/shuhei/orgs","repos_url":"https://api.github.com/users/shuhei/repos","events_url":"https://api.github.com/users/shuhei/events{/privacy}","received_events_url":"https://api.github.com/users/shuhei/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":8,"created_at":"2016-07-16T16:23:10Z","updated_at":"2016-07-19T15:10:59Z","closed_at":null,"body":"**Do you want to request a *feature* or report a *bug*?**  \r\nfeature\r\n\r\n\r\n**What is the current behavior?**\r\nI really like the idea of background epics because it provides an intuitive way to handle multiple actions like debouncing with the powerful operators of Rx. However, tests for epics require mocking HTTP requests or other side effects.\r\n\r\nTo address the same problem, `redux-saga` allows sagas to `yield` effects as data, such as `call`, executes the effects, and feed the results to sagas. This keeps sagas side-effect-free and testable without mocking.\r\n\r\n**If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via https://jsbin.com or similar.**\r\nIt's not a bug.\r\n\r\n**What is the expected behavior?**\r\n\r\nTo employ the same approach to `redux-observable`, the straight forward way would be express effects as actions. We can have a dedicated middleware to receive side effect requests, execute them, and `dispatch`es results as actions so that we can throw out side effects of epics.\r\n\r\n```js\r\nimport { call } from 'redux-observable/effects';\r\n\r\nfunction epic(action$, store) {\r\n  const fooReq$ = action$.ofType('FOO')\r\n    .map(action => call('FOO_REQ', webapi.getFoo, action.payload.id));\r\n  const foo$ = action$.ofType('FOO_REQ')\r\n    .map(foo => ({ type: 'FOO_FETCHED', payload: foo }));\r\n\r\n  return Observable.merge(\r\n    fooReq$,\r\n    foo$\r\n  );\r\n}\r\n```\r\n\r\nThe example above should work but it's tedious to have pairs of request observable and response observable. I want to write the pair as a single `Observable`. A possible solution would be creating an operator that emits effect requests to a `Subject` and returns an `Observable` that emits effect responses. Let's call it as `effect` operator. Here I assumed that `effect` returns an `Observable` of `Observable`s in order to express effect's result as an `Observable`.\r\n\r\n```js\r\nimport { call } from 'redux-observable/effects';\r\n\r\nfunction epic(action$, store) {\r\n  const effect$ = new Subject();\r\n\r\n  const foo$ = action$.ofType('FOO')\r\n    .effect(action$, effect$, action => call(webapi.getFoo, action.payload.id))\r\n    .switch()\r\n    .map(foo => ({ type: 'FOO_FETCHED', payload: foo }));\r\n\r\n  return Observable.merge(\r\n    effect$,\r\n    foo$\r\n  );\r\n}\r\n```\r\n\r\nI wrote quick sketches for the effect middleware and operator.\r\n\r\n<details>\r\n\r\n```js\r\nconst EFFECT_REQUEST = Symbol('EFFECT_REQUEST');\r\nconst EFFECT_RESPONSE = Symbol('EFFECT_RESPONSE');\r\n\r\nconst effectMiddleware = store => next => action => {\r\n  if (typeof action[EFFECT_REQUEST] !== 'object') {\r\n    return next(action);\r\n  }\r\n\r\n  const { id, payload } = action[EFFECT_REQUEST];\r\n  const { type, args } = payload;\r\n  switch (type) {\r\n    case 'call':\r\n      try {\r\n        const returnValue = args[0].apply(null, args.slice(1));\r\n        if (isObservable(returnValue)) {\r\n          next({\r\n            [EFFECT_RESPONSE]: { id, type, payload: returnValue }\r\n          });\r\n        } else if (typeof returnValue.then === 'function') {\r\n          returnValue\r\n            .then(result => next({\r\n              [EFFECT_RESPONSE]: { id, type, payload: Observable.of(result) }\r\n            }))\r\n            .catch(error => next({\r\n              [EFFECT_RESPONSE]: { id, type, payload: Observable.throw(error) }\r\n            }));\r\n        } else {\r\n          next({\r\n            [EFFECT_RESPONSE]: { id, type, payload: Observable.of(returnValue) }\r\n          });\r\n        }\r\n      catch (error) {\r\n        next({\r\n          [EFFECT_RESPONSE]: { id, type, payload: Observable.throw(error) }\r\n        });\r\n      }\r\n    default:\r\n      // TODO: Handle other effect types.\r\n      return next(action);\r\n  }\r\n};\r\n```\r\n\r\n```js\r\nfunction effectOperator(action$, effect$, callback) {\r\n  return Observable.create(subscriber => {\r\n    const id = uuid();\r\n    const source = this;\r\n    const responseSub = action$\r\n      .filter(a => a[EFFECT_RESPONSE] && a[EFFECT_RESPONSE].id === id)\r\n      .map(a => a[EFFECT_RESPONSE].payload)\r\n      .subscribe(subscriber);\r\n    const requestSub = source.subscribe(\r\n      value => {\r\n        try {\r\n          effect$.next({\r\n            [EFFECT_REQUEST]: { id, payload: callback(value) }\r\n          });\r\n        } catch (e) {\r\n          subscriber.error(e);\r\n        }\r\n      },\r\n      err => subscriber.error(err),\r\n      () => subscriber.complete()\r\n    );\r\n    // https://github.com/ReactiveX/rxjs/issues/1583\r\n    return new Subscription(() => {\r\n      [responseSub, requestSub].forEach(s => s.unsubscribe());\r\n    });\r\n  });\r\n}\r\nObservable.prototype.effect = effectOperator;\r\n```\r\n\r\n</details>\r\n\r\nWhat do you think about this idea?\r\n\r\nThere are still some things to consider:\r\n\r\n- Awkwardness of passing `action$` and `effect$`\r\n- Awkwardness of explicitly creating a `Subject`\r\n- Epic composition\r\n\r\n**Which versions of redux-observable, and which browser and OS are affected by this issue? Did this work in previous versions of redux-observable?**\r\nIt's not a bug.\r\n"},"comment":{"url":"https://api.github.com/repos/redux-observable/redux-observable/issues/comments/233664101","html_url":"https://github.com/redux-observable/redux-observable/issues/76#issuecomment-233664101","issue_url":"https://api.github.com/repos/redux-observable/redux-observable/issues/76","id":233664101,"user":{"login":"shuhei","id":498635,"avatar_url":"https://avatars.githubusercontent.com/u/498635?v=3","gravatar_id":"","url":"https://api.github.com/users/shuhei","html_url":"https://github.com/shuhei","followers_url":"https://api.github.com/users/shuhei/followers","following_url":"https://api.github.com/users/shuhei/following{/other_user}","gists_url":"https://api.github.com/users/shuhei/gists{/gist_id}","starred_url":"https://api.github.com/users/shuhei/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/shuhei/subscriptions","organizations_url":"https://api.github.com/users/shuhei/orgs","repos_url":"https://api.github.com/users/shuhei/repos","events_url":"https://api.github.com/users/shuhei/events{/privacy}","received_events_url":"https://api.github.com/users/shuhei/received_events","type":"User","site_admin":false},"created_at":"2016-07-19T15:10:59Z","updated_at":"2016-07-19T15:10:59Z","body":"@jayphelps Thanks a lot for your comments and thinking about this! Sorry for this late reply. I've been busy today and have to go to bed soon...\r\n\r\nAs you mentioned, it's about declarative effects and invoking them outside of epics. It's just a dirty sketch but we will be able to test it like the following. I omitted mechanics to match request and response here to simplify it.\r\n\r\n```js\r\nit('fetches foo', () => {\r\n  // Let's say we have a Subject with ofType.\r\n  const input$ = new ActionsSubject();\r\n  const store = {};\r\n  const outputs = [];\r\n  fetchFooEpic(input$, store).subscribe(action => outputs.push(action));\r\n\r\n  input$.next({ type: 'FETCH_FOO', payload: { id: 123 });\r\n  expect(outputs[0].payload).toEqual(call(webapi.getFoo, 123));\r\n\r\n  const payload = { id: 123, name: 'foo' };\r\n  // TODO: We need something like UUID to match request and response.\r\n  input$.next(callResponse(outputs[0].id, payload));\r\n  expect(outputs[1]).toEqual({ type: 'FETCH_FOO_FULFILLED', payload });\r\n  expect(outputs.length).toEqual(2);\r\n});\r\n\r\nit('cancels request', () => {\r\n  const input$ = new ActionsSubject();\r\n  const store = {};\r\n  const outputs = [];\r\n  fetchFooEpic(input$, store).subscribe(action => outputs.push(action));\r\n\r\n  input$.next({ type: 'FETCH_FOO', payload: { id: 123 });\r\n  expect(outputs[0]).toEqual(call(webapi.getFoo, 123));\r\n  input$.next({ type: 'FETCH_FOO_CANCELED' });\r\n  const payload = { id: 123, name: 'foo' };\r\n  input$.next(callResponse(outputs[0].id, payload));\r\n  // The response is ignored here.\r\n  expect(outputs.length).toEqual(1);\r\n});\r\n```\r\n\r\nAlso, we'll be able to feed an error to the `Subject` and test error handling with an operator like the `Details` section of this issue.\r\n\r\nI'll try to answer your other questions tomorrow."}}
{"actor":{"display_login":"shuhei"}}