{"id":28673333,"url":"https://github.com/webstudio-is/immerhin","last_synced_at":"2025-06-13T20:10:19.267Z","repository":{"id":52378469,"uuid":"475373423","full_name":"webstudio-is/immerhin","owner":"webstudio-is","description":"🔂  Send patches around to keep the system in sync.","archived":false,"fork":false,"pushed_at":"2024-11-19T13:57:29.000Z","size":110,"stargazers_count":63,"open_issues_count":0,"forks_count":4,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-06-06T05:49:54.567Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/webstudio-is.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-03-29T09:35:11.000Z","updated_at":"2025-04-03T08:05:23.000Z","dependencies_parsed_at":"2024-06-19T06:17:40.188Z","dependency_job_id":"0dbfa94e-9397-4a37-a1aa-6150f4d0d0cb","html_url":"https://github.com/webstudio-is/immerhin","commit_stats":{"total_commits":27,"total_committers":3,"mean_commits":9.0,"dds":"0.37037037037037035","last_synced_commit":"fe97496f363441f30aecdb578f814b55a659635b"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/webstudio-is/immerhin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webstudio-is%2Fimmerhin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webstudio-is%2Fimmerhin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webstudio-is%2Fimmerhin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webstudio-is%2Fimmerhin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/webstudio-is","download_url":"https://codeload.github.com/webstudio-is/immerhin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/webstudio-is%2Fimmerhin/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259712415,"owners_count":22900041,"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":"2025-06-13T20:10:18.730Z","updated_at":"2025-06-13T20:10:19.249Z","avatar_url":"https://github.com/webstudio-is.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003csection align=\"center\"\u003e\n  \u003cimg src=\"https://user-images.githubusercontent.com/52824/161781684-27b7a682-f066-4da8-ab8a-df6f6e5d1511.svg\" /\u003e\n\u003c/section\u003e\n\nThe core idea is to use [patches](https://immerjs.github.io/immer/patches) to keep the UI in sync between client and server, multiple clients, or multiple windows.\n\nIt uses [Immer](https://immerjs.github.io/immer/) as an interface for state mutations and provides a convenient way to group mutations into a single transaction, and enables undo/redo out of the box.\n\n[Play with it on Codesandbox](https://codesandbox.io/s/github/webstudio-is/immerhin/tree/main/examples/react)\n\n[Read the article](https://dev.to/oleg008/synchronized-immutable-state-with-time-travel-2c6o)\n\n## Features\n\n1. Sync application state using [patches](https://immerjs.github.io/immer/patches)\n1. Get undo/redo for free\n1. Sync to the server\n1. Server agnostic\n1. State management libraries agnostic (a container interface)\n1. Small bundle size\n1. Sync between iframes (not implemented yet)\n1. Sync between tabs (not implemented yet)\n1. Resolve conflicts (not implemented yet)\n1. Provide server handler (not implemented yet)\n\n## Example\n\n```js\nimport store, { sync } from \"immerhin\";\n\n// Create containers for each state. Sync engine only cares that the result has a \"get()\" and a \"set(newValue)\"\nconst container1 = atom(initialValue);\nconst container2 = atom(initialValue);\n\n// - Explicitely enable containers for transactions\n// - Define a namespace for each container, so that server knows which object it has to patch.\nstore.register(\"container1\", container1);\nstore.register(\"container2\", container2);\n\n// Creating the actual transaction that will:\n// - generate patches\n// - update states\n// - inform all subscribers\n// - register a transaction for potential undo/redo and sync calls\nstore.createTransaction(\n  [container1, container2, ...rest],\n  (value1, value2, ...rest) =\u003e {\n    mutateValue(value1);\n    mutateValue(value2);\n    // ...\n  }\n);\n\n// Setup periodic sync with a fetch, or do this with Websocket\nsetInterval(async () =\u003e {\n  const entries = sync();\n  await fetch(\"/patch\", { method: \"POST\", payload: JSON.stringify(entries) });\n}, 1000);\n\n// Undo/redo\n\nstore.undo();\nstore.redo();\n```\n\n## How it works\n\n### Containers\n\nA container is an interface that implements `.get()` and `.set(value)` methods so that a value can be updated and propagated to all consumers.\n\nYou can use anything to create containers, it could be a Redux store, could be an observable, a [nano state](https://github.com/kof/react-nano-state) or [nanostores](https://github.com/nanostores/nanostores).\n\nYou can use the same container instance to subscribe to the changes across the entire application.\n\nExample using nano state:\n\n```js\nimport { atom } from \"nanostores\";\nimport { useStore } from \"@nanostores/react\";\nconst myContainer = atom(initialValue);\n\n// I can call a set from anywhere\nmyContainer.set(newValue);\n\n// I can subscribe to updates in React\nconst Component = () =\u003e {\n  const value = useStore(myContainer);\n};\n```\n\n### Container registration\n\nWe register containers for two reasons:\n\n1. To define a namespace for each container so that whoever consumes the changes knows which object to apply the patches to.\n2. Ensure that the container was intentionally registered to be synced to the server and be part of undo/redo transactions. You may not want this for every container since you can use them for ephemeral states.\n\nExample\n\n```js\nstore.register(\"myName\", myContainer);\n```\n\n### Creating a transaction\n\nA transaction is a set of changes applied to a set of states. When you apply changes to the states inside a transaction, you are essentially telling the engine which changes are associated with the same user action so that undo/redo can use that as a single step to work with.\n\nA call into `store.createTransaction()`does all of this:\n\n- generate patches (using Immer)\n- update states and inform all subscribers (by calling `container.set(newValue)`)\n- register a transaction for potential undo/redo and calls\n\nExample\n\n```js\nstore.createTransaction(\n  [container1, container2, ...rest],\n  (value1, value2, ...rest) =\u003e {\n    mutateValue(value1);\n    mutateValue(value2);\n    // ...\n  }\n);\n```\n\n### Undo/redo\n\nCalling undo() and redo() functions will essentially apply the right patch for the value and dispatch the update.\n\n### Sync\n\nThe `sync(`) function returns you all changes queued up for a sync since the last call.\nWith the return from `sync(),` you can do anything you want, for example, send it to your server.\n\nExample\n\n```js\n// Setup periodic sync with a fetch, or do this with Websocket\nsetInterval(async () =\u003e {\n  const entries = sync();\n  await fetch(\"/patch\", { method: \"POST\", payload: JSON.stringify(entries) });\n}, 1000);\n```\n\nExample entries:\n\n```json\n[\n  {\n    \"transactionId\": \"6243062b469f516835327f65\",\n    \"changes\": [\n      {\n        \"namespace\": \"root\",\n        \"patches\": [\n          {\n            \"op\": \"replace\",\n            \"path\": [\"children\", 1],\n            \"value\": {\n              \"component\": \"Box\",\n              \"id\": \"6241f55791596f2467df9c2a\",\n              \"style\": {},\n              \"children\": []\n            }\n          },\n          {\n            \"op\": \"replace\",\n            \"path\": [\"children\", 2],\n            \"value\": {\n              \"component\": \"Box\",\n              \"id\": \"6241f55a91596f2467df9c36\",\n              \"style\": {},\n              \"children\": []\n            }\n          },\n          {\n            \"op\": \"replace\",\n            \"path\": [\"children\", \"length\"],\n            \"value\": 3\n          }\n        ]\n      }\n    ]\n  }\n]\n```\n\n## Create a new store\n\nIf you want to have multiple separate undoable states, create a separate store for each. They add to the same sync queue in the end.\n\n```js\nimport { Store } from \"immerhin\";\n\nconst store = new Store();\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebstudio-is%2Fimmerhin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwebstudio-is%2Fimmerhin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebstudio-is%2Fimmerhin/lists"}