{"id":28797648,"url":"https://github.com/maluscat/reactive-storage","last_synced_at":"2026-03-10T01:31:07.491Z","repository":{"id":295447694,"uuid":"989852355","full_name":"Maluscat/reactive-storage","owner":"Maluscat","description":"[MIRROR] Register, observe and intercept deeply reactive data without the need for proxies","archived":false,"fork":false,"pushed_at":"2025-06-05T21:13:34.000Z","size":147,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-21T11:58:36.329Z","etag":null,"topics":["data","javascript","reactive","typescript"],"latest_commit_sha":null,"homepage":"https://gitlab.com/Maluscat/reactive-storage","language":"JavaScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Maluscat.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,"zenodo":null}},"created_at":"2025-05-25T00:55:55.000Z","updated_at":"2025-06-05T21:13:37.000Z","dependencies_parsed_at":"2025-05-25T16:36:02.341Z","dependency_job_id":"c424915b-0341-49d1-ab5b-d767814daa32","html_url":"https://github.com/Maluscat/reactive-storage","commit_stats":null,"previous_names":["maluscat/reactive-storage"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Maluscat/reactive-storage","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Maluscat%2Freactive-storage","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Maluscat%2Freactive-storage/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Maluscat%2Freactive-storage/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Maluscat%2Freactive-storage/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Maluscat","download_url":"https://codeload.github.com/Maluscat/reactive-storage/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Maluscat%2Freactive-storage/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30320885,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T20:05:46.299Z","status":"ssl_error","status_checked_at":"2026-03-09T19:57:04.425Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["data","javascript","reactive","typescript"],"created_at":"2025-06-18T04:32:29.217Z","updated_at":"2026-03-10T01:31:07.066Z","avatar_url":"https://github.com/Maluscat.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ReactiveStorage\nRegister, observe and intercept deeply reactive data on any object without the\nneed for\n[proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)!\n\n\n```js\nconst storage = new ReactiveStorage({\n  depth: Infinity,\n  setter: ({ val, path }) =\u003e { console.log(`SET ${path.join('.')}:`, val) }\n});\nstorage.register('foo');\n\nstorage.target.foo = {\n  bar: 3,\n  baz: [ 'a', 'b' ]\n};\n// SET foo: { ... }\n// SET foo.bar: 3\n// SET foo.baz: [ ... ]\n// SET foo.baz.0: \"a\"\n// SET foo.baz.1: \"a\"\n\nstorage.target.foo.bar++;\n// SET foo.bar: 4\n\nstorage.target.foo.baz[1] = 'lor';\n// SET foo.baz.1: \"lor\"\n```\n\n\n## Contents\n- [Rationale](#rationale)\n- [Limitations](#limitations)\n- [Installation](#installation)\n- [Concepts](#concepts)\n- [Usage](#usage)\n  - [Instanced vs. static](#instanced-vs-static-approach)\n  - [Registering properties](#registering-properties)\n  - [Configuring deep values](#configuring-deep-values)\n  - [Reactivity is kept alive](#reactivity-is-kept-alive)\n  - [Initial assignment](#initial-assignment)\n  - [Intercepting values](#intercepting-values)\n  - [Multiple sequential targets](#multiple-sequential-targets)\n  - [Instance helper functions](#instance-helper-functions)\n  - [Using with types](#using-with-types)\n- [Configuration](#configuration)\n- [Examples](#examples)\n- [Docs](#docs)\n\n\n## Rationale\n[Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy)\nare dope and allow for full reactivity, but they come with a significant\nperformance overhead. Even though JS is very fast nowadays, property accesses\nadd up quickly as they are used constantly and everywhere. Sure,\nReactiveStorage is somewhat limited when compared to proxies, but it can be\nvery powerful when used in the right spots!\n\nI've also seen some sources claim deep reactivity to be impossible without using\nproxies. Even Vue\n[didn't support it](https://v2.vuejs.org/v2/guide/reactivity.html#For-Arrays)\nbefore making the switch to proxies. Take that!\n\n\n## Limitations\nReactiveStorage is explicitly not a catch-all solution for reactivity. Since it\npurely relies on\n[Object.defineProperty](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty),\nit inherits all of its limitations too. These largely amount to:\n- No dynamic approach; Data must be explicitly registered in order to become\n  reactive.\n- Only getters and setters; The data cannot be modified in-place using methods\n  like `array.push(...)` or `array.splice(...)`.\n\n\n## Installation\nThis library can be used inside any JavaScript environment, including the web.\nThe work on the library is mostly finished, so there won't be many more updates\nin the future.\n\n### Download\nThe only required file is `ReactiveStorage.js` inside the [`script`](./script)\nfolder. If you want type checking, fetch `ReactiveStorage.d.ts` as well!\n\n### npm\nAvailable on npm under `@maluscat/reactive-storage`. Use your favorite package\nmanager (or use it with Deno):\n```sh\nyarn add @maluscat/reactive-storage\nbun install @maluscat/reactive-storage\nnpm install @maluscat/reactive-storage\n```\n\n\n## Concepts\nA reactive property is defined via `Object.defineProperty` with attached\ngetters/setters. Properties are defined on a target object and store their\nactual values at an arbitrary place, the endpoint. In this library, the\nendpoint is always an object, and a property's values are stored under the\nproperty name within that endpoint (see below).\n\n\u003e [!note]\n\u003e In most of this document, \"object\" refers to *any* JavaScript object,\n\u003e be it an object literal, an array, a class instance, etc.\n\nLet's say we register a reactive property \"foo\". The data flows like this:\n```1c\n# \"foo\" on target = {}\n\ntarget.foo \u003c-[GET]-- endpoint.foo\ntarget.foo --[SET]-\u003e endpoint.foo\n```\n\nArrays work analogously since they are just objects with special syntax:\n```1c\n# \"0\" on target = []\n\ntarget[0] \u003c-[GET]-- endpoint[0]\ntarget[0] --[SET]-\u003e endpoint[0]\n```\n\nInstead of only having a single target that points to an endpoint, we can\nscale horizontally by sequentially routing a value through\n[multiple targets](#multiple-sequential-targets):\n```1c\n# \"foo\" on:\n# 1. target0 = {}\n# 2. target1 = {}\n\ntarget0.foo \u003c-[GET]-- target1.foo \u003c-[GET]-- endpoint.foo\ntarget0.foo --[SET]-\u003e target1.foo --[SET]-\u003e endpoint.foo\n```\n\n[Deep reactivity](#configuring-deep-values) is a recursive registration of\nobject values to make their children reactive as well (vertical scaling). The\nproperties of each distinct object are registered into a new storage object\nwhich their respective parents point to:\n```1c\n# \"foo = { bar: 3, baz: 4 }\" on target = {}\n# implicit: storage1 = {} with { GET/SET bar, GET/SET baz }\n\ntarget.foo \u003c-[GET]-- storage1\ntarget.foo --[SET]-\u003e endpoint.foo\n\nstorage1.bar \u003c-[GET]-- endpoint.foo.bar\nstorage1.bar --[SET]-\u003e endpoint.foo.bar\n\nstorage1.baz \u003c-[GET]-- endpoint.foo.baz\nstorage1.baz --[SET]-\u003e endpoint.foo.baz\n```\n\nLooking only at the getters makes it a bit clearer:\n```1c\n# \"foo = { bar: 3, baz: { lor: 10 } }\" on target = {}\n# implicit: storage1 = {} with { GET/SET bar, GET/SET baz }\n# implicit: storage2 = {} with { GET/SET lor }\n\ntarget.foo \u003c-[GET]-- storage1\nstorage1.bar \u003c-[GET]-- endpoint.foo.bar\nstorage1.baz \u003c-[GET]-- storage2\nstorage2.lor \u003c-[GET]-- endpoint.foo.baz.lor\n```\n\n\n## Usage\nThe only non-typing exports are `ReactiveStorage`, `Filter` (also exposed via\n`ReactiveStorage.Filter`) and, if needed, `ReactiveStorageError`:\n```js\nimport { ReactiveStorage, Filter, ReactiveStorageError } from '@maluscat/reactive-storage';\n```\nSee the [docs](#docs) for an overview of all additional typing related exports\nfor use in TypeScript.\n\nReactiveStorage can make properties reactive such that they invoke callbacks\nwhenever they are accessed or assigned to. In addition, object values can be\nmade [deeply reactive](#configuring-deep-values) – This allows any change within\narbitrarily deeply nested properties to be caught and intercepted. *Any* object\ncan be deeply registered, though by default only arrays and object literals will\npropagate to avoid infinite recursion and unwanted overhead (can be controlled\nwith the [`depthFilter`](#depthfilter) config option). In addition, properties\ncan be sequentially routed through\n[multiple targets](#multiple-sequential-targets), each with their own\nconfiguration.\n\n### Instanced vs. static approach\nThere are two ways to use this library: Using a `ReactiveStorage` instance or\nusing static methods.\n\nAn instance takes a single immutable [configuration](#configuration) in its\nconstructor which is used every time a property is registered. The static\nmethods take the configuration on a per-registration basis.\n\nThe used configuration is stored in the `config` instance property. The\ntarget(s) and the shallow endpoint of the first layer are additionally exposed\nvia the `targets`, `target` and `shallowEndpoint` properties. `target` always\npoints to the first item of `targets`, so unless you're using [multiple\ntargets](#multiple-sequential-targets), you can always use that one.\n\n```js\nimport { ReactiveStorage, Filter } from './ReactiveStorage.js';\n\nconst storage = new ReactiveStorage({\n  target: {},\n  shallowEndpoint: {},\n  enumerable: true,\n  depth: 0,\n  depthFilter: Filter.objectLiteralOrArray,\n  getter: undefined,\n  setter: undefined,\n  postSetter: undefined,\n});\n```\n\n### Registering properties\nSee the [examples](#examples) for more info.\n\n#### Instance\nThe instance method `register(...)` uses its instance's\n[configuration](#configuration) and returns the instance to allow for chaining.\nThe initial value is optional.\n```ts\nregister(\n  key: number | string | symbol | (number | string | symbol)[],\n  initialValue?: any\n): ReactiveStorage\n```\n\n`registerFrom(...)` can be used to register all properties (including symbols)\nof a given object on the instance's target.\n```ts\nregisterFrom(data: object): ReactiveStorage\n```\n\n#### Static\nThe static methods optionally take a [configuration](#configuration) and return\nan object containing the used targets and the shallow endpoint of the first\nlayer, in which `target` points to the first item in `targets`.\n```ts\nregister(\n  key: number | string | symbol | (number | string | symbol)[],\n  initialValue?: any,\n  options: RegistrationOptions = {}\n): { targets: object[], target: object, shallowEndpoint: object }\n```\n```ts\nregisterFrom(\n  data: object,\n  options: RegistrationOptions = {}\n): { targets: object[], target: object, shallowEndpoint: object }\n```\n\nThere are helper functions to extend each of the above defined functions with\ninfinitely deep recursion (same as `depth: Infinity` in the deepest `depth`).\n```ts\nregisterRecursive(\n  key: number | string | symbol | (number | string | symbol)[],\n  initialValue?: any,\n  options?: RegistrationOptions = {}\n): { targets: object[], target: object, shallowEndpoint: object }\n```\n```ts\nregisterRecursiveFrom(\n  data: object,\n  options: RegistrationOptions = {}\n): { targets: object[], target: object, shallowEndpoint: object }\n```\n\n### Configuring deep values\nTo register a property deeply, you can use the [`depth`](#depth) config option\nwhich accepts either a number or a configuration. A deep configuration will\nregister any properties (including symbols) of an assigned object again,\nprovided that it matches the [`depthFilter`](#depthfilter) config option.\n\nIf given a *number*, this will be the max depth until which assigned values will\nbe registered. In this case, a layer's options except `target` and\n`shallowEndpoint` will be inherited from its parent config.\n\nA given *configuration* will define options for that specific layer, which is\nuseful to specify individual getters/setters for each layer of depth. The\n`target` and `shallowEndpoint` options must not be specified (since they will\nchange with each new assignment). Missing options can be inherited using the\nspecial keyword `\"inherit\"`, with the exception of `enumerable`, which will\nalways be inherited unless overridden.\n\n\u003e [!tip]\n\u003e Instead of extensively nesting depth configurations, you can also make use of\n\u003e the [getter/setter](#setter) `path` argument (specifically, its length).\n\nHere, three explicit reactivity layers are defined, each of which define an\nindividual setter while inheriting the topmost getter. Layer 2 defines one\nadditional implicit layer. Any layers below that won't be reactive:\n```js\nconst storage = new ReactiveStorage({\n  depth: {\n    depth: {\n      setter: ({ val, path }) =\u003e { console.log(`Layer 2 or 3 SET ${path.join('.')}:`, val) },\n      getter: 'inherit',\n      depth: 1\n    },\n    setter: ({ val, path }) =\u003e { console.log(`Layer 1 SET ${path.join('.')}:`, val) },\n    getter: 'inherit',\n  },\n  setter: ({ val, path }) =\u003e { console.log(`Layer 0 SET ${path.join('.')}:`, val) },\n  getter: ({ val, path }) =\u003e { console.log(`GET ${path.join('.')}:`, val) },\n});\n\nstorage.register('foo', { bar: 3 });\n// Layer 0 SET foo: { bar: 3 }\n// Layer 1 SET foo.bar: 3\n\nstorage.target.foo = { bar: { baz: { lor: { val: 9 } } } }\n// Layer 0      SET foo: { bar: ... }\n// Layer 1      SET foo.bar: { baz: ... }\n// Layer 2 or 3 SET foo.bar.baz: { lor: ... }\n// Layer 2 or 3 SET foo.bar.baz.lor: { val: 9 }\n/// \u003cLayer 4 and downwards is not reactive\u003e\n```\n\n### Reactivity is kept alive\nThe initial registration [configuration](#configuration) is always kept alive,\nmeaning that reassigning a value will register it with the configuration used in\nits initial registration. This also means that providing an initial value is\noptional – If omitted, an initial setter call will not be invoked (same with\nexplicitly passing `undefined`).\n```js\nconst storage = new ReactiveStorage({\n  depth: Infinity,\n  setter: ({ val, path }) =\u003e { console.log(`SET ${path.join('.')}:`, val) },\n});\n\nstorage.register('foo');\n\nstorage.target.foo = 3;\n// SET foo: 3\n\n// \u003cNote how this is reactive again!\u003e\nstorage.target.foo = [ { lor: 69 }, 'bar', 'baz' ];\n// SET foo: [ ... ]\n// SET foo.0: { foo: 69 }\n// SET foo.0.lor: 69\n// SET foo.1: \"bar\"\n// SET foo.2: \"baz\"\n```\n\n### Initial assignment\nThe initial assignment will already call the specified `setter` and `postSetter`\n(unless the initial value is omitted or `undefined`). This can be filtered using\nthe callback functions' `initial` parameter.\n```js\nconst storage = new ReactiveStorage({\n  depth: Infinity,\n  setter: ({ val, initial, path }) =\u003e {\n    console.log(`${initial ? 'initial' : ''} SET ${path.join('.')}:`, val)\n  },\n  postSetter: ({ val, initial, path }) =\u003e {\n    console.log(`${initial ? 'initial' : ''} POST-SET ${path.join('.')}:`, val)\n  }\n});\n\nstorage.register('foo', {\n  bar: [ 10, 20 ],\n  baz: {\n    lor: 'my-string'\n  }\n});\n// initial      SET foo: { bar: ..., baz: ... }\n// initial      SET foo.bar: [ 10 ]\n// initial      SET foo.bar.0: 10\n// initial POST-SET foo.bar.0: 10\n// initial POST-SET foo.bar: [ 10 ]\n// initial      SET foo.baz: { lor: ... }\n// initial      SET foo.baz.lor: \"my-string\"\n// initial POST-SET foo.baz.lor: \"my-string\"\n// initial POST-SET foo.baz: { lor: ... }\n// initial POST-SET foo: { bar: ..., baz: ... }\n\nstorage.target.foo = 3;\n//      SET foo: 3\n// POST-SET foo: 3\n```\n\n### Intercepting values\nBy default, a **setter** is passive: after being called, the passed value will\nautomatically be set to the property's current endpoint. However, a setter\ncan return `true` to prevent the value from being set. In addition, a\nmodified/custom value can be assigned instead using the passed default setter\n`set` (after which `true` should always be returned to prevent setting a value\ntwice).\n\nA **getter** analogously only observes the fetched values passively by default,\nbeing given the value of the underlying endpoint when a property is fetched.\nHowever, any return value other than a nullish value (`null` or `undefined`)\nwill yield this value to the caller.\n\nHere, any assigned value that isn't a number will be discarded, while numbers\nwill always be clamped to the range [0, 100]. When fetched, they will be rounded\nto the nearest 5:\n```js\nconst storage = new ReactiveStorage({\n  getter: ({ val }) =\u003e {\n    return Math.round(val / 5) * 5;\n  },\n  setter: ({ val, set }) =\u003e {\n    if (typeof val !== 'number') return true;\n    if (val \u003e 100) {\n      set(100);\n      return true;\n    } else if (val \u003c 0) {\n      set(0);\n      return true;\n    }\n  },\n});\nstorage.register('foo', 38);\nconsole.log(storage.shallowEndpoint.foo) // 38\nconsole.log(storage.target.foo) // 40\n\nstorage.target.foo = -6;\nconsole.log(storage.shallowEndpoint.foo) // 0\nconsole.log(storage.target.foo) // 0\n\nstorage.target.foo = 52;\nconsole.log(storage.shallowEndpoint.foo) // 52\nconsole.log(storage.target.foo) // 50\n\nstorage.target.foo = 'bar'\nconsole.log(storage.shallowEndpoint.foo) // 52\nconsole.log(storage.target.foo) // 50\n```\n\n### Multiple sequential targets\nIt's easy to setup multiple target points by passing multiple respective\nconfigurations, which a value is sequentially routed through until it reaches\nthe endpoint (See \"horizontal scaling\" in [Concepts](#concepts)).\n\nAll defined targets (one for each passed configuration) are stored in the\n`targets` property of either the returned data when using the static methods or\nof the created instance. The `target` property always points to the first\nelement in `targets` and can be conveniently used when only one target has been\ndefined.\n\n\u003e [!tip]\n\u003e Consider utilizing this system and configuring an additional target instead of\n\u003e using a (shallow) endpoint.\n\nHere, two layers are defined. The first does high-level work such as validating\nits values while the second does some mandatory operations. In one possible\nscenario, the first target could be exposed to the user as a high-level\ninterface while the second is used for internal purposes where the validity of\nan assigned value is already ensured:\n```js\nconst storage = new ReactiveStorage([\n  {\n    setter: ({ val }) =\u003e {\n      return !inputIsValid(val);\n    },\n  }, {\n    setter: ({ val, path }) =\u003e {\n      propertyHasNewValue(path, val);\n    },\n  }\n]);\n\nstorage.register('foo');\n\nstorage.targets[0].foo = 3;\n// First go through `inputIsValid`, then `propertyHasNewValue`\n\nstorage.targets[1].foo = 4;\n// Only `propertyHasNewValue` is called\n```\n\n### Instance helper functions\nThe `has(...)` instance method returns true if the given property key exists on\nthe instance's `target` and has thus been registered, false otherwise.\n```ts\nhas(key: number | string | symbol): boolean\n```\n\nThe `delete(...)` instance method deletes a registered property from the\ninstance's `target` and `shallowEndpoint`. Returns true if a property was\nsuccessfully deleted (speak, if the property had been registered), false\notherwise.\n\nDeep properties will not be deleted because the class does not hold a reference\nto them. As such, they will be garbage collected instead.\n```ts\ndelete(key: number | string | symbol): boolean\n```\n\n### Using with types\nSince TypeScript is an entirely static language, there is no way to propagate\ntype information from an instance method to an instance property. This is why,\nwithout additional information, only the `target`/`targets` returned by the two\nstatic methods `ReactiveStorage.register(...)` and\n`ReactiveStorage.registerRecursive(...)` know about the registered properties.\n\nTo supply additional type information, a property-value interface can be passed\nas a generic to the `ReactiveStorage` class or the static methods mentioned\nabove.\n\nSadly, TS is quite limited when it comes to generics and cannot match the given\nproperty names to the given value when using static functions; To do that, you\nneed to pass the property names into the generic as well (it's so dumb).\n\n```ts\ninterface Properties {\n  foo: number\n  bar: string[]\n  baz: Array\u003cnumber\u003e | number\n}\n\nconst storage = new ReactiveStorage\u003cProperties\u003e();\nstorage.register('lor');     // ERROR: Not a known property!\nstorage.register('bar', {}); // ERROR: Type for 'bar' does not match!\nstorage.register('foo', 4);\n\n// `storage.target` is typed to contain the properties\n// 'foo', 'bar', 'baz' and their respective types\n\n// Analogously:\nReactiveStorage.register\u003cProperties, 'foo'\u003e('foo', 4);\n```\n\n\n## Configuration\nThe configuration can either be passed to the constructor or to the static\nmethods. See here a summarized version of its interface with the full one\navailable at the [docs](#docs) (along with more examples). All fields are\noptional.\n\n### `target`\n- Type: `object`\n- Default: `{}`\n\nThe access point for the registered property/properties. Values are deposited at\nthe [endpoint](#shallowEndpoint) *shallowly*. Can be *any* object, so it may\nalso be an array, a class instance, etc.\n\n\u003e [!note]\n\u003e This property may only be defined in the topmost level of a configuration and\n\u003e not within [`depth`](#depth).\n\n### `shallowEndpoint`\n- Type: `object`\n- Default: `{}`\n\nThe object that holds the actual data of registered properties *shallowly*. The\nconfigured setter and getter will deposit the value to and fetch the value from\nthis endpoint respectively.\n\nConsider using a [second target](#multiple-sequential-targets) instead.\n\n\u003e [!important]\n\u003e The shallow endpoint's value will NOT be updated if a sub-property is changed.\n\u003e As a rule of thumb, this property should rarely every be used, and never so if\n\u003e any sort of depth is configured. This would lead to errors and confusion.\n\n\u003e [!note]\n\u003e This property may only be defined in the topmost level of a configuration and\n\u003e not within [`depth`](#depth).\n\n### `enumerable`\n- Type: `boolean`\n- Default: `true`\n\nWhether registered properties should be enumerable inside the [target](#target).\nCorresponds to the\n[`Object.defineProperty` option](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty#enumerable)\nof the same name.\n\n### `depth`\n- See also [Configuring deep values](#configuring-deep-values)\n- Type: `Configuration | number`\n- Default: `0`\n\nWhether and how keys inside object values should be registered such that they go\nthrough additional layers of getters and setters. If a value is reassigned, it\nis re-registered with the same configuration until the configured depth.\nThe properties `target` and `shallowEndpoint` are not allowed.\n\n**If given a configuration**, the registered property will assume these options\nin its layer. Can be nested infinitely deep. Options except `enumerable` can be\nexplicitly inherited from the parent config with the keyword `\"inherit\".`\n\n**If given a number**, keys will be registered recursively up until the given\ndepth, inheriting the parent options. Can be `Infinity`.\n\n### `depthFilter`\n- Type: `(obj: object, path: Array\u003cstring | symbol\u003e) =\u003e boolean`\n- Default: `Filter.objectLiteralOrArray`\n\nDecide whether to deeply register an object covered by [`depth`](#depth).\nThis is useful to mitigate registering properties within *any* object (class\ninstances, DOM nodes, etc.) in favor of simpler objects.\n\nBe careful when changing this, especially when there is user input involved!\nUnrestricted recursion may lead to a significant overload or even an infinite\nloop when (accidentally) assigning complex objects like a DOM node.\n\n### `postSetter`\n- Type: `(event: PostSetterEvent) =\u003e void`\n\nCalled *after* a value has been set.\n\nThe passed event object has the following properties:\n- `val`: The value that was set\n- `prevVal`: The previous value\n- `initial` (`boolean`): Whether this call is propagated by the initial\n    registration\n- `path` (`Array\u003cstring | symbol\u003e`): Key path of the property that was set\n\n### `setter`\n- See also [Intercepting values](#intercepting-values)\n- Type: `(event: SetterEvent) =\u003e void | boolean`\n\nCalled *before* a value is set. Return `true` to discard the value, i.e. to stop\nthe default action of setting the value to the underlying endpoint.\n\nThe passed event object has the following properties:\n- `val`: The value that will be set (unless discarded)\n- `prevVal`: The previous value\n- `initial` (`boolean`): Whether this call is propagated by the initial\n    registration\n- `path` (`Array\u003cstring | symbol\u003e`): Key path of the property that was set\n- `set` (`(val) =\u003e void`): Default setter that sets a given value to the\n    underlying endpoint. When using it, you should prevent the default value\n    from being set by returning `true`.\n\n### `getter`\n- See also [Intercepting values](#intercepting-values)\n- Type: `(event: GetterEvent) =\u003e void | any`\n\nCalled anytime a value is fetched. Return `null` or `undefined` to propagate the\ndefault value. Any other return value will be the property's value.\n\nDeep properties require a lot of `getter` calls, so when using depth\nextensively, you should probably keep inherited getter functions lightweight.\n\nThe passed event object has the following properties:\n- `val`: The value from the underlying endpoint\n- `path` (`Array\u003cstring | symbol\u003e`): Key path of the property that was set\n\n\n## Examples\n### Basic example using `registerFrom`\nUsing `registerFrom` to register every property of a predefined object. Notice\nhow `\"bar\"` does not invoke an initial setter call since its initial value is\nundefined.\n```js\nconst data = {\n  foo: 3,\n  bar: undefined,\n  [Symbol.for('baz')]: 'unique Symbol!'\n};\n\n// We just extract `target` because we only have a single target\n// and don't care about `shallowEndpoint`\nconst { target } = ReactiveStorage.registerFrom(data, {\n  setter: ({ val, path }) =\u003e { console.log(`${path}: ${val}`) }\n});\n// ['foo']: 3\n// Symbol: 'unique Symbol!'\n```\n\n### Deep arrays with `registerFrom` and multiple targets\nArrays work analogously. Here we use a nested depth configuration and multiple\ntargets. We assume that we only ever work with arrays in this scenario, so we\nset each target to an empty array as opposed to an empty object literal (even\nthough both are functionally equivalent).\n\nThe first target simply reports the values it gets. The second caps any received\nnumber to 100 and the third discards any strings set *in the first vertical layer*\nsince it does not configure any depth. Keep in mind that getters and setters\npropagate in contrary directions, so a setter trickles down: `target[0] -\u003e\ntarget[1] -\u003e target[2] -\u003e endpoint` while a getter bubbles up: `target[0] \u003c-\ntarget[1] \u003c- target[2] \u003c- endpoint`.\n```js\nconst data = ReactiveStorage.registerFrom([ 0, 1, 2, 3 ], [\n  {\n    target: [],\n    depth: {\n      getter: false\n    },\n    getter: ({ val }) =\u003e {\n      console.log(\"Layer 0:\", val);\n    }\n  }, {\n    target: [],\n    depth: 1,\n    getter: ({ val }) =\u003e {\n      if (typeof val === 'number') {\n        return Math.min(val, 100);\n      }\n    }\n  }, {\n    target: [],\n    setter: ({ val }) =\u003e {\n      if (typeof val === 'string') {\n        console.log(\"Dropping string!\");\n        return true;\n      }\n    },\n    getter: ({ val }) =\u003e {\n      console.log(\"Layer 2:\", val);\n    }\n  }\n]);\n\n/* Getters invoked by setting a value have been omitted */\nconsole.log(data.targets[0][0]); // 0\n// Layer 2: 0\n// Layer 0: 0\ndata.targets[0][0] = 120;\nconsole.log(data.targets[0][0]); // 100\n// Layer 2: 120\n// Layer 0: 100\nconsole.log(data.targets[2][0]); // 120\n// Layer 2: 120\n\ndata.targets[0][1] = [ 200 ];\nconsole.log(data.targets[0][1][0]); // 100\n// Layer 0: 100\n\ndata.targets[0][2] = -3;\ndata.targets[0][2] = 'foobar';\n// Dropping string!\nconsole.log(data.targets[0][2]); // -3\n// Layer 2: -3\n// Layer 0: -3\n```\n\n### Register deep properties using an instance\nThis example makes use of a `ReactiveStorage` instance instead of calling a\nstatic registration method directly. This gives us the ability to register\nmultiple properties as well as delete them again at any time.\n\nHere, the setters of the first two depth layers are configured manually while\nchanges in layer 2 and below invoke a catch-all setter. Property paths (and with\nthat, also their depth) can be determined by the `path` argument. If the\nrespective depth was set to, say, `2` instead of `Infinity`, only layers 2, 3\nand 4 would be made reactive (the defined depth layer + 2).\n```js\nconst storage = new ReactiveStorage({\n  depth: {\n    depth: {\n      depth: Infinity,\n      setter: ({ val, path }) =\u003e {\n        console.log(`Catch-all layer ${path.length - 1}:`, val);\n      }\n    },\n    setter: ({ val }) =\u003e {\n      console.log(\"Layer 1:\", val);\n    }\n  },\n  setter: ({ val }) =\u003e {\n    console.log(\"Layer 0:\", val);\n  }\n});\n\nstorage\n  .register(['foo', 'bar', 'baz'])\n  .register('lor', 3);\n// Layer 0: 3\n\nstorage.target.foo = {\n  foo1: {\n    foo2: {\n      foo3: [ 10, 20, 30 ]\n    }\n  }\n};\n// Layer 0: { foo1: ... }\n// Layer 1: { foo2: ... }\n// Catch-all layer 2: { foo3: ... }\n// Catch-all layer 3: [ 10, 20, 30 ]\n// Catch-all layer 4: 10\n// Catch-all layer 4: 20\n// Catch-all layer 4: 30\n```\n\n\n## Docs\nSee the generated [docs](https://docs.malus.zone/reactive-storage/) for a more\nin-depth overview of the library.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaluscat%2Freactive-storage","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaluscat%2Freactive-storage","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaluscat%2Freactive-storage/lists"}