{"id":19303387,"url":"https://github.com/jsheaven/observed","last_synced_at":"2025-09-14T07:22:02.636Z","repository":{"id":65812205,"uuid":"600211197","full_name":"jsheaven/observed","owner":"jsheaven","description":"Transparently observe and/or intercept JavaScript Object and Array mutations (data changes) at any level (depth)","archived":false,"fork":false,"pushed_at":"2023-02-17T10:07:59.000Z","size":57,"stargazers_count":11,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-13T23:24:14.867Z","etag":null,"topics":["object","observer","proxy","reflect"],"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/jsheaven.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":"2023-02-10T20:48:44.000Z","updated_at":"2024-09-17T00:00:07.000Z","dependencies_parsed_at":"2023-02-21T02:01:06.157Z","dependency_job_id":null,"html_url":"https://github.com/jsheaven/observed","commit_stats":{"total_commits":9,"total_committers":1,"mean_commits":9.0,"dds":0.0,"last_synced_commit":"b9da5da4b9cf6d2619a28c9ad4c22a0fc62d41ab"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsheaven%2Fobserved","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsheaven%2Fobserved/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsheaven%2Fobserved/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jsheaven%2Fobserved/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jsheaven","download_url":"https://codeload.github.com/jsheaven/observed/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250232196,"owners_count":21396588,"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":["object","observer","proxy","reflect"],"created_at":"2024-11-09T23:26:13.693Z","updated_at":"2025-04-22T11:32:04.214Z","avatar_url":"https://github.com/jsheaven.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e@jsheaven/observed\u003c/h1\u003e\n\n\u003e Transparently observe and/or intercept JavaScropt Object and Array mutations (data changes) at any level (depth)\n\n\u003ch2 align=\"center\"\u003eUser Stories\u003c/h2\u003e\n\n1. As a developer, I want to define a store as an object at one location in code, but want hooks to be called somewhere else, when values in that object change at any level I'm interested in, atomically (e.g. `foo.bar.xyz[1]`).\n\n2. As a developer, I want the ability to intercept mutations and change the value of the mutation before it happens (meta-programming, indirection).\n\n3. As a developer, I want to observe mutations `before` and `after` they happen. I want the ability to listen for mutation globally and/or atomically to specific locations in the object tree.\n\n4. As a developer, I want the library to act transparently, so that every mutation (set) comes thru, even though the value may be the same.\n\n5. As a developer, I expect great performance without unnecessary overhead/dependencies. I expect `O(1)` runtime complexity on average.\n\n\u003ch2 align=\"center\"\u003eFeatures\u003c/h2\u003e\n\n- ✅ Observes mutations (value changes) in JavaScript Objects and Arrays at any level\n- ✅ Available as a simple, functional API: `const myObservedObj = observed({ foo: { test: 123, bar: [true] } });`\n- ✅ Observe changes at any level `foo.test` etc.\n- ✅ Allows to stop observing at any level using `offSet(myObservedObj.foo)`\n- ✅ Allows to register interceptors to change mutation values\n- ✅ Allows to register _global_ listeners and interceptors\n- ✅ Allows to control depth of observation\n- ✅ Just `720 byte` nano sized (ESM, gizpped)\n- ✅ 0 dependencies\n- ✅ Tree-shakable and side-effect free\n- ✅ First class TypeScript support\n- ✅ 100% Unit Test coverage\n\n\u003ch2 align=\"center\"\u003eExample usage\u003c/h2\u003e\n\n\u003ch3 align=\"center\"\u003eSetup\u003c/h3\u003e\n\n- yarn: `yarn add @jsheaven/observed`\n- npm: `npm install @jsheaven/observed`\n\n\u003ch3 align=\"center\"\u003eESM\u003c/h3\u003e\n\n```ts\nimport { observed, onSet, offSet } from '@jsheaven/observed'\n\nconst someObject = { foo: 'X', cool: { test: 123 } }\n\n// is now observed, by default deeply\nconst someObjectObserved = observed(someObject)\n\nconst onSomethingInCoolChange = (prop, value, prevValue) =\u003e {\n  console.log(prop, 'changed from', prevValue, 'to', value)\n}\n\n// listen to changes, by default in 'after' phase, when it already happened\nonSet(someObjectObserved.cool, onSomethingInCoolChange)\n\nsomeObjectObserved.cool.test = 456 // prints to console now\n\n// lets set an interceptor, so we can change the values before the change happens\nconst interceptAndChangeWhenSomethingInCoolChanges = (prop, value, prevValue) =\u003e {\n  console.log(prop, 'changed from', prevValue, 'to', value)\n  if (prop === 'test' \u0026\u0026 typeof value === 'number') {\n    return value * 2 // always double\n  }\n  return value // original change\n}\nonSet(someObjectObserved.cool, interceptAndChangeWhenSomethingInCoolChanges, 'before')\n\nsomeObjectObserved.cool.test = 456 // prints 912 to console now because the doubling of the value happens first\n\nsomeObjectObserved.lala = 234 // nothing happens, there is no listener on someObject directly\n\nsomeObjectObserved.foo = 'Y' // nothing happens, also here there is no listener\n\n// you can also test an object to be observed (if Proxy the proxy is set)\nisObserved(someObjectObserved) // true\nisObserved(someObject) // false\n\n// and also fetch the callback references from an observed object\ngetObservers(someObjectObserved.cool) // [ ... ]\n\n// to stop listenting/intercepting specifically:\noffSet(someObjectObserved.cool, onSomethingInCoolChange)\n\n// to remove all callbacks on that level at once:\noffSet(someObjectObserved.cool)\n```\n\n\u003ch3 align=\"center\"\u003eCommonJS\u003c/h3\u003e\n\n```ts\nconst { observed } = require('@jsheaven/observed')\n\n// same API like ESM variant\n```\n\n\u003ch3 align=\"center\"\u003eAdvanced - global listeners and depth control\u003c/h3\u003e\n\n```ts\nimport { observed } from '@jsheaven/observed'\n\n// original object always stays untouched (never mutated from observed objects mutations)\nconst someObject = { foo: 'X', cool: { test: 123 } }\n\n// now lets say we that we want to observe any change at any level\nconst someObjectObserved = observed(someObject, {\n  onSet: (prop, value, prevValue) =\u003e {\n    console.log(prop, 'changed from', prevValue, 'to', value)\n  },\n  onGet: (prop, value, target, receiver) =\u003e {\n    console.log(prop, 'has been read/accessed with value', value, 'in target object', target, 'receiver', receiver)\n  },\n  onBeforeSet: (prop, value, prevValue) =\u003e {\n    console.log(prop, 'changed from', prevValue, 'to', value)\n    if (typeof value === 'number') {\n      return value * 2 // always double\n    }\n    return value // original change\n  },\n})\n\n// both listeners are registered now and act before and after all atomic listeners\n// therefore atomic listeners in 'before' phase, can override global listeners in 'before' phase\n// and global listeners in 'after' phase always receive the \"final\" result and are reading last\n\nsomeObjectObserved.cool.test = 456 // prints 912 to console now because the doubling of the value happens first\n\nsomeObjectObserved.lala = 468 // prints 468\n\nsomeObjectObserved.foo = 'Y' // prints to the console\n\n// has no effect, because we're talking global listeners and they can't be removed\noffSet(someObjectObserved.cool)\n```\n\n\u003ch3 align=\"center\"\u003eCommonJS\u003c/h3\u003e\n\n```ts\nconst { observed } = require('@jsheaven/observed')\n\n// same API like ESM variant\n```\n\n\u003ch2 align=\"center\"\u003eNotes on time complexity\u003c/h2\u003e\n\nTL;DR: On average, the time complexity of this library is `O(1)`.\n\nBecause this library supports infinite depth mutation tracking, and because you could add as much hook listeners on every property of an object as you like, you can, theoretical, run into `O(n)` where n is the number of properties on the object being observed. This is because the observed function loop over all the properties of the object to observe them. The time complexity of the `onSet` function is `O(n)` as it adds a callback to the list of callbacks of the observed object, which has a length of `n`, but as explained before, in the average case, it's `O(1)`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsheaven%2Fobserved","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjsheaven%2Fobserved","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjsheaven%2Fobserved/lists"}