{"id":21076610,"url":"https://github.com/monojack/immerx-observable","last_synced_at":"2025-05-16T07:31:35.078Z","repository":{"id":42819622,"uuid":"267860170","full_name":"monojack/immerx-observable","owner":"monojack","description":"Observable based middleware for Immerx","archived":false,"fork":false,"pushed_at":"2023-01-06T07:21:51.000Z","size":1303,"stargazers_count":5,"open_issues_count":12,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-12T04:45:45.720Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/monojack.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-29T13:07:00.000Z","updated_at":"2023-03-07T13:30:53.000Z","dependencies_parsed_at":"2023-02-05T15:02:16.533Z","dependency_job_id":null,"html_url":"https://github.com/monojack/immerx-observable","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monojack%2Fimmerx-observable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monojack%2Fimmerx-observable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monojack%2Fimmerx-observable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monojack%2Fimmerx-observable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/monojack","download_url":"https://codeload.github.com/monojack/immerx-observable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254488240,"owners_count":22079387,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-19T19:29:24.060Z","updated_at":"2025-05-16T07:31:34.651Z","avatar_url":"https://github.com/monojack.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"images/immerx-observable-logo.svg\" height=\"70px\"/\u003e\n\nObservable based **middleware** for [ImmerX](https://github.com/monojack/immerx).\n\n\u003cbr/\u003e\n\n**Table of contents**:\n\n- [`Install`](#install)\n- [`Setup`](#setup)\n- [`Epics`](#epics)\n  - [`read/write epic`](#readwrite-epic)\n  - [`read-only epic`](#read-only-epic)\n  - [`combine epics`](#combine-epics)\n  - [`patchOf() operator`](#patchof-operator)\n  - [`accessing the state`](#accessing-the-state)\n  - [`dependency injection`](#dependency-injection)\n  - [`error-handling`](#error-handling)\n\n\u003cbr/\u003e\n\n### `Install`\n\n```sh\nnpm install @immerx/observable\n```\n\n\u003cbr/\u003e\n\n### `Setup`\n\nThe middleware setup process is pretty similar to the one of [redux-observable](https://redux-observable.js.org/) - which is what inspired this library.\n\n```js\nimport { createObservableMiddleware } from '@immerx/observable'\n\nconst middleware = createObservableMiddleware()\n```\n\nWe use a **very** basic [Observable](https://github.com/tc39/proposal-observable) implementation under the hood and exposes a `setAdapter` function that we can use to transform it into the type required by the library we are using (`rxjs`, `xstream`, `most` etc.).\n\n```js\nimport { from } from 'rxjs'\nimport { setAdapter } from '@immerx/observable'\n\nsetAdapter(from)\n```\n\nProvide the middleware to the `create` function from `@immerx/state`\n\n```js\nimport { create } from '@immerx/state'\n\nconst initialState = {}\ncreate(initialState, [middleware])\n```\n\nAnd then run it with the `rootEpic`\n\n```js\nimport rootEpic from './epics'\n\nmiddleware.run(rootEpic)\n```\n\nAll together:\n\n```js\nimport { create } from '@immerx/state'\nimport { createObservableMiddleware, setAdapter } from '@immerx/observable'\nimport { from } from 'rxjs'\n\nimport rootEpic from './epics'\n\nconst middleware = createObservableMiddleware()\nconst initialState = {} // or whatever initial state\n\ncreate(initialState, [middleware])\n\nsetAdapter(from) // make sure we set the adapter before calling middleware.run()\nmiddleware.run(rootEpic)\n```\n\n\u003cbr/\u003e\n\n### `Epics`\n\nImmerx notifies middleware of all, non-empty, [immer patches](https://immerjs.github.io/immer/docs/patches) and the new state value after those patches have been applied. So `@immerx/observable` pipes all the patches through an observable that gets passed as the **first argument** to our epics. Epics should return an observable of [producers](https://immerjs.github.io/immer/docs/produce), unless we want a `read-only` epic that only performs some side-effects and doesn't necessarily update the state. Let's see examples of both:\n\n#### `read/write epic`\n\nImagine we have an `auth` property in our state that we'll assign the result of decoding an auth token. That result may contain a user `uid` that we can use to fetch the data/profile for that user. Here's the initial state:\n\n```js\nconst initialState = {\n  auth: null,\n  //... maybe some other stuff\n}\n```\n\nWhenever we initialize/change the value of `.auth` immerx will notify all middleware with the following patch object:\n\n```js\n{\n  op: 'replace', // 'add' when initialized\n  path: ['auth'],\n  value: {\n    uid: '5eba786ea1b6f1314dac9b7b',\n    // ...other stuff\n  },\n}\n```\n\nHere's a basic implementation of a `fetchUserDataEpic`:\n\n**NOTE**: I will be using `rxjs` to `adapt` in the following examples, so I assume you already know how the various operators I'll be using work. If `rxjs` is not your thing, you should still have an idea about what the operators might do - they are pretty much the same across all observable/stream libraries but their names may vary. If you are completely new to observables, then maybe you should look into that first.\n\n```js\n// epics.js\nimport { REPLACE } from '@immerx/observable'\n\nconst fetchUserDataEpic = patch$ =\u003e\n  patch$.pipe(\n    filter(patch =\u003e patch.op === REPLACE \u0026\u0026 /^auth/.test(patch.path.join('.'))),\n    switchMap(patch =\u003e ajax.getJSON(`https://userservice/${patch.value.uid}`)),\n    map(userData =\u003e draft =\u003e void (draft.userData = userData)),\n  )\n```\n\nA patch can have one of three types: `add`, `replace` and `remove`. For convenience, `@immerx/observable` exports the `ADD`, `REPLACE` and `REMOVE` constants.\n\nWe `filter` the `patch$` observable because we want to listen for only `replace` patches that have been applied to the `auth` piece of our state. Then we fetch the user data for the user `uid` that we get from the new `.auth` value. Then we map the fetched `userData` into a [curried producer](https://immerjs.github.io/immer/docs/curried-produce) which will get passed to `@immerx/state` and be used to update the state.\n\n#### `read-only epic`\n\nSometimes, we want our epic to only perform some side-effects without ultimately updating the state. There are more ways to do this. If we're using `rxjs` we can just slap an `ignoreElements()` at the end of our pipe, or just `skipUntil(NEVER)`, or `filter(() =\u003e false)`, or remember that producers are just functions and we can always send down a `noop`.\n\nFollowing the above example, let's say we want to cache the user data somewhere and maybe just get it from the cache next time we have to fetch it.\n\n```js\n// epics.js\nimport { ADD, REPLACE } from '@immerx/observable'\nimport { userCache } from './caches'\n\nfunction noop() {}\nconst cacheUserDataEpic = patch$ =\u003e\n  patch$.pipe(\n    filter(\n      patch =\u003e\n        [ADD, REPLACE].includes(patch.op) \u0026\u0026\n        /^userData/.test(patch.path.join('.')),\n    ),\n    pluck('value'),\n    tap(userData =\u003e userCache.set(userData.id, userData)),\n    map(() =\u003e noop),\n    // or ignoreElements()\n    // or skipUntil(NEVER)\n    // or filter(() =\u003e false)\n  )\n\nconst fetchUserDataEpic = patch$ =\u003e\n  patch$.pipe(\n    filter(patch =\u003e patch.op === REPLACE \u0026\u0026 /^auth/.test(patch.path.join('.'))),\n    pluck('value', 'uid'),\n    switchMap(uid =\u003e\n      userCache.has(uid)\n        ? of(userCache.get(uid))\n        : ajax.getJSON(`https://userservice/${uid}`),\n    ),\n    map(userData =\u003e draft =\u003e void (draft.userData = userData)),\n  )\n```\n\n#### `combine epics`\n\nSo now we have two epics, but the `middleware.run()` method takes only one (a `rootEpic`). `@immerx/observable` exports a function which we can use to combine multiple epics into one.\n\n```js\n// epics.js\nimport { combineEpics } from '@immerx/observable'\n\n// ...\nexport default combineEpics(fetchUserDataEpic, cacheUserDataEpic)\n```\n\n#### `patchOf() operator`\n\nThe epic filters look kinda ugly, let's replace them with the `patchOf()` operator.\n\n```js\n// epics.js\nimport { ADD, REPLACE } from '@immerx/observable'\nimport { patchOf } from '@immerx/observable/operators'\nimport { userCache } from './caches'\n\nconst cacheUserDataEpic = patch$ =\u003e\n  patch$.pipe(\n    patchOf({ ops: [ADD, REPLACE], path: ['userData'] }),\n    pluck('value'),\n    tap(userData =\u003e userCache.set(userData.id, userData)),\n    ignoreElements(),\n  )\n\nconst fetchUserDataEpic = patch$ =\u003e\n  patch$.pipe(\n    patchOf({ op: REPLACE, path: ['auth'] }),\n    pluck('value', 'uid'),\n    switchMap(uid =\u003e\n      userCache.has(uid)\n        ? of(userCache.get(uid))\n        : ajax.getJSON(`https://userservice/${uid}`),\n    ),\n    map(userData =\u003e draft =\u003e void (draft.userData = userData)),\n  )\n```\n\nThe operator takes an object with `op` or `ops` (if we want to allow more operation types) and a `path` and tries to match them against the patch properties. FWIW, we can omit either `path` or `op/ops` and it will filter accordingly, or omit everything - in which case it will pass down the `source` observable directly.\n\n**NOTE**: The `patchOf()` operator implementation is very simple, it's basically the same filter we had before + some extra checks and handlers - you can check it out in the source code. However, it is built around our basic Observable implementation. It tries to accommodate for some cases (uses the `.filter()` method if found on the source observable), but if none apply it will create a new Observable. Whether you're ok with that or not is totally up to you, but if you're being a purist, then you might want to implement it yourself by leveraging the internal mechanisms and structures of your observable library.\n\nHere is a similar implementation as an `rxjs` pipeable operator:\n\n```js\nimport { filter } from 'rxjs/operators'\n\nconst EMPTY_OBJ = {}\nfunction patchOf(o = EMPTY_OBJ) {\n  return function patchOfOperator(source) {\n    if (o === EMPTY_OBJ) {\n      return source\n    }\n\n    const { op, ops = [op], path = [] } = o\n    return source.pipe(\n      filter(\n        patch =\u003e\n          (ops.filter(Boolean).length === 0 || ops.includes(patch.op)) \u0026\u0026\n          RegExp(`^${path.join('.')}`).test(patch.path.join('.')),\n      ),\n    )\n  }\n}\n```\n\n#### `accessing the state`\n\nSometimes we may need access to the current state value to use in our epic. Say we want to prevent fetching user data if we already have it in our state. The **second argument** passed to our epics is a stream of state values.\n\n```js\nconst fetchUserDataEpic = (patch$, state$) =\u003e\n  patch$.pipe(\n    patchOf({ op: REPLACE, path: ['auth'] }),\n    pluck('value', 'uid'),\n    withLatestFrom(state$),\n    filter(([uid, state]) =\u003e (state.userData || {}).id !== uid),\n    switchMap(([uid]) =\u003e\n      userCache.has(uid)\n        ? of(userCache.get(uid))\n        : ajax.getJSON(`https://userservice/${uid}`),\n    ),\n    map(userData =\u003e draft =\u003e void (draft.userData = userData)),\n  )\n```\n\n#### `dependency injection`\n\nYou may have noticed that we are importing the `userCache` from a local `caches` module. Not a very big deal, that's how we do things, right? Right, until we have to test our epics and need to mock most of their dependencies - we don't want to depend on `userCache` to behave correctly, more so when we're using an external caching mechanism/service. Also, we might not want to hit the API and really fetch the user data in our tests, so we would need to find a way to mock `ajax.getJSON` too.\n\nThe better approach is to inject the dependencies into the epics and we can do that through `createObservableMiddleware`'s configuration object:\n\n```js\nimport { userCache } from './caches'\nimport { ajax } from 'rxjs/ajax'\n\n// ...\nconst middleware = createObservableMiddleware({\n  dependencies: {\n    userCache,\n    ajax,\n  },\n})\n```\n\nThe `dependencies` map gets passed in as the **third argument** to our epics.\n\n```js\nconst cacheUserDataEpic = (patch$, _, { userCache }) =\u003e\n  patch$.pipe(\n    // ...\n    tap(userData =\u003e userCache.set(userData.id, userData)),\n    // ...\n  )\n\nconst fetchUserDataEpic = (patch$, state$, { ajax } = {}) =\u003e\n  patch$.pipe(\n    // ...\n    switchMap(([uid]) =\u003e\n      userCache.has(uid)\n        ? of(userCache.get(uid))\n        : ajax.getJSON(`https://userservice/${uid}`),\n    ),\n    // ...\n  )\n```\n\nNow all of our epics are using the injected dependencies and we can write our tests without the headaches.\n\n#### `error handling`\n\nAlways important. Especially when dealing with http requests. While there are several ways of handling errors, the most common way is to catch and handle them inside our epics:\n\n```js\nimport { EMPTY } from 'rxjs'\n\nconst fetchUserDataEpic = (patch$, state$) =\u003e\n  patch$.pipe(\n    patchOf({ op: REPLACE, path: ['auth'] }),\n    pluck('value', 'uid'),\n    switchMap(([uid]) =\u003e\n      userCache.has(uid)\n        ? of(userCache.get(uid))\n        : ajax.getJSON(`https://userservice/${uid}`).pipe(\n            catchError(error =\u003e {\n              // maybe do something with the error?\n              return EMPTY\n            }),\n          ),\n    ),\n    map(userData =\u003e draft =\u003e void (draft.userData = userData)),\n  )\n```\n\nWe are catching the error, maybe log it to the console or send it to our logging service, whatever, but we also return an [EMPTY](https://rxjs.dev/api/index/const/EMPTY) observable, which is enough to bail out and make sure we don't reach the `map()` with our producer. In some cases we might want to `retry()`, or send down a default value, or even something that would help use branch out inside the `map()`, it's up to us to decide.\n\n**NOTE**: It is **very** important to add the `catchError()` to the `getJSON().pipe()` inside the `switchMap()` because if we let the error reach the `patch$.pipe()` it will terminate it and will stop listening for new patches - we don't want that... or do we?\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmonojack%2Fimmerx-observable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmonojack%2Fimmerx-observable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmonojack%2Fimmerx-observable/lists"}