{"id":24083352,"url":"https://github.com/mitranim/espo","last_synced_at":"2025-10-09T14:02:34.596Z","repository":{"id":57230493,"uuid":"78742696","full_name":"mitranim/espo","owner":"mitranim","description":"Observables via proxies. Particularly suited for UI programming. Supports automatic, implicit sub/resub and resource deinit.","archived":false,"fork":false,"pushed_at":"2022-02-15T16:25:54.000Z","size":441,"stargazers_count":11,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-09T14:02:26.120Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","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/mitranim.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","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":"2017-01-12T12:20:04.000Z","updated_at":"2023-05-02T19:03:07.000Z","dependencies_parsed_at":"2022-09-01T21:51:01.735Z","dependency_job_id":null,"html_url":"https://github.com/mitranim/espo","commit_stats":null,"previous_names":[],"tags_count":50,"template":false,"template_full_name":null,"purl":"pkg:github/mitranim/espo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fespo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fespo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fespo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fespo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mitranim","download_url":"https://codeload.github.com/mitranim/espo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fespo/sbom","scorecard":{"id":95114,"data":{"date":"2022-08-15","repo":{"name":"github.com/mitranim/espo","commit":"3254cae45989a730e03cb0a82c36c2fd669ea275"},"scorecard":{"version":"v4.5.0-17-g7772984","commit":"777298477c07c262a4ec7e95ceee839b7b3b75ae"},"score":4.5,"checks":[{"name":"Code-Review","score":0,"reason":"no reviews found","details":["Warn: no reviews found for commit: 3254cae45989a730e03cb0a82c36c2fd669ea275","Warn: no reviews found for commit: e965a5cbf1d47e1cd3550c07cd0c9cddcab80df2","Warn: no reviews found for commit: 990951a082906030d6716f71fa376c4c1b9020d5","Warn: no reviews found for commit: 6ca9111aa655bbf41ba7a9ad0f92edb5f36295fb","Warn: no reviews found for commit: f43414cc3dfef004a4faa0635bb3d9e628074691","Warn: no reviews found for commit: 69f967e500b9ea2ae10bc7d04919162140429ead","Warn: no reviews found for commit: e959618940eae06cbae90d22c5d913e70acae2bb","Warn: no reviews found for commit: f3632f81b821be85f44bc8413137e18886b4d875","Warn: no reviews found for commit: cc0b03f70f1d94deb96bc8809ee7b22bafb2cfc5","Warn: no reviews found for commit: efd0ca4b8f66e0ccd8c045b5c6d2641383bdb281","Warn: no reviews found for commit: 67e60336d81dc501daad007f0d0069c6c26014bb","Warn: no reviews found for commit: 78e7387fe97d413321f6d3c6f41f1534afb777ab","Warn: no reviews found for commit: fc96c31bf7319293775e9e73736a2d7f1ed86d56","Warn: no reviews found for commit: 9296d5481f4322abac8a0e0759788aa27254f09b","Warn: no reviews found for commit: 641418a09d295e54f57a8fe74612ccbabc65be9c","Warn: no reviews found for commit: e362b8bea2b2073620442a3941841b249d4de6e0","Warn: no reviews found for commit: 51c78be986f969c42d70a795f0560e5593ffca6d","Warn: no reviews found for commit: c4c73dab18f2b0a8cde1ddaedef2205afe03c5cb","Warn: no reviews found for commit: fd9321517c95dae0003c64639064e30ea1e6adea","Warn: no reviews found for commit: dfcb7be0203a0c913968f20dcf62dea5fbb1e339","Warn: no reviews found for commit: 7c444187fd9beb11228823624cccc35c989fb315","Warn: no reviews found for commit: 489a855307a68581b723f0b969d6ac290c5c15f2","Warn: no reviews found for commit: d2e15840aae60911f27566be5320933dfc87448d","Warn: no reviews found for commit: 0a9dd9581981d581a235ab19c97707d461a89ee7","Warn: no reviews found for commit: 23f70b450ac435b3ea141a24c896452bfafcfd64","Warn: no reviews found for commit: 8bad69d89de7a2f434cd5edc960361749d576520","Warn: no reviews found for commit: 915d2babd3cb7db9b87307d1836ee396616d236d","Warn: no reviews found for commit: f246373cd88304821d95433ce69889aa094d21a2","Warn: no reviews found for commit: a86dc18e5fe4769286421322a4eab5bdb9f1879f","Warn: no reviews found for commit: 426c9e3642f483df857401eb348193acf2d586ef"],"documentation":{"short":"Determines if the project requires code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#code-review"}},{"name":"Maintained","score":0,"reason":"0 commit(s) out of 30 and 0 issue activity out of 0 found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#maintained"}},{"name":"CII-Best-Practices","score":0,"reason":"no badge detected","details":null,"documentation":{"short":"Determines if the project has a CII Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#cii-best-practices"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":["Warn: no GitHub releases found"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#signed-releases"}},{"name":"Vulnerabilities","score":10,"reason":"no vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#vulnerabilities"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#branch-protection"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"no published package detected","details":["Warn: no GitHub publishing workflow detected"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":10,"reason":"tokens are read-only in GitHub workflows","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":10,"reason":"all dependencies are pinned","details":["Info: GitHub-owned GitHubActions are pinned","Info: Third-party GitHubActions are pinned","Info: Dockerfile dependencies are pinned","Info: no insecure (not pinned by hash) dependency downloads found in Dockerfiles","Info: no insecure (not pinned by hash) dependency downloads found in shell scripts"],"documentation":{"short":"Determines if the project has declared and pinned its dependencies.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#pinned-dependencies"}},{"name":"License","score":0,"reason":"license file not detected","details":null,"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#license"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":null,"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#security-policy"}},{"name":"Dependency-Update-Tool","score":0,"reason":"no update tool detected","details":["Warn: dependabot config file not detected in source location.\n\t\t\tWe recommend setting this configuration in code so it can be easily verified by others.","Warn: renovatebot config file not detected in source location.\n\t\t\tWe recommend setting this configuration in code so it can be easily verified by others."],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#dependency-update-tool"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":null,"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/777298477c07c262a4ec7e95ceee839b7b3b75ae/docs/checks.md#fuzzing"}}]},"last_synced_at":"2025-08-15T08:49:07.120Z","repository_id":57230493,"created_at":"2025-08-15T08:49:07.120Z","updated_at":"2025-08-15T08:49:07.120Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279001525,"owners_count":26083117,"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","status":"online","status_checked_at":"2025-10-09T02:00:07.460Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":[],"created_at":"2025-01-09T23:56:26.913Z","updated_at":"2025-10-09T14:02:34.577Z","avatar_url":"https://github.com/mitranim.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Overview\n\nObservables via [proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). Particularly suited for UI programming. Features:\n\n* Implicit sub/resub, just by accessing properties.\n* Implicit trigger, by property modification.\n* Any object can be made observable; see [`obs`](#function-obsref). Subclassing is available but not required.\n* Arbitrary properties can be observed. No need for special annotations. (Opt-out via non-enumerables.)\n* Implicit resource cleanup: automatically calls `.deinit()` on removed/replaced objects.\n* Your objects are squeaky clean, with no added library junk other than mandatory `.deinit()`.\n* Nice-to-use in plain JS. Doesn't rely on decorators, TS features, etc.\n* Easy to wire into any UI system. Comes with optional adapters for React (`react.mjs`) and custom DOM elements (`dom.mjs`).\n\nTiny (a few KiB _un_-minified) and dependency-free. Native JS module.\n\nKnown limitations:\n\n* No special support for collections.\n* When targeting IE, requires transpilation and ES2015 polyfills.\n\n## TOC\n\n* [Usage](#usage)\n  * [Install](#install)\n  * [Trichotomy of proxy/handler/target](#trichotomy-of-proxyhandlertarget)\n  * [Implicit sub/resub](#implicit-subresub)\n  * [Auto-deinit](#auto-deinit)\n  * [Enumerable vs non-enumerable](#enumerable-vs-non-enumerable)\n  * [`new Proxy` vs function vs subclass](#new-proxy-vs-function-vs-subclass)\n* [API](#api)\n  * [`interface isDe`](#interface-isdeval)\n  * [`interface isObs`](#interface-isobsval)\n  * [`interface isTrig`](#interface-istrigval)\n  * [`interface isSub`](#interface-issubval)\n  * [`interface isSubber`](#interface-issubberval)\n  * [`interface isRunTrig`](#interface-isruntrigval)\n  * [`function ph`](#function-phref)\n  * [`function self`](#function-selfref)\n  * [`function de`](#function-deref)\n  * [`function obs`](#function-obsref)\n  * [`function comp`](#function-compref-fun)\n  * [`function lazyComp`](#function-lazycompref-fun)\n  * [`class Deinit`](#class-deinit)\n  * [`class Obs`](#class-obs)\n  * [`class Comp`](#class-compfun)\n  * [`class LazyComp`](#class-lazycompfun)\n  * [`class Loop`](#class-loopref-issub)\n  * [`class Moebius`](#class-moebiusref-isruntrig)\n  * [`class DeinitPh`](#class-deinitph)\n  * [`class ObsPh`](#class-obsph)\n  * [`class CompPh`](#class-compphfun)\n  * [`class LazyCompPh`](#class-lazycompphfun)\n  * [`class Sched`](#class-sched)\n  * [`const sch`](#const-sch)\n  * [`function mut`](#function-mutref-src)\n  * [`function deinit`](#function-deinitval)\n  * [Undocumented](#undocumented)\n\n## Usage\n\n### Install\n\n```sh\nnpm i -E espo\n```\n\nEspo uses native JS modules, which work both in Node and browsers. It's even usable in browsers without a bundler; use either an importmap (polyfillable), an exact file path, or a full URL.\n\n```js\nimport * as es from 'espo'\n\nimport * as es from './node_modules/espo/espo.mjs'\n\nimport * as es from 'https://cdn.jsdelivr.net/npm/espo@0.8.2/espo.mjs'\n```\n\n### Trichotomy of proxy/handler/target\n\nWhile Espo is fairly magical (🧚 fairy magical?), the user must be aware of proxies. In Espo, your objects aren't \"observables\" in a classic sense, full of special properties and methods. Instead, they remain clean, but wrapped into a `Proxy`, together with a _proxy handler_ object, which is the actual state of the observable, with subscribers, explicit sub/unsub methods, and so on.\n\nMostly, you wrap via [`obs`](#function-obsref) or by subclassing [`Obs`](#class-obs), and just use the resulting proxy as if it was the original object. [Implicit sub/resub](#implicit-subresub) relies on proxy features. For explicit sub/unsub, access the proxy handler via [`ph`](#function-phref), and call the handler's methods.\n\n```js\nimport * as es from 'espo'\n\nconst target = {someProp: 'someVal'}\n\n// Same as `es.obs(target)`, but without support for `.onInit`/`.onDeinit`.\nconst obs = new Proxy(target, new es.ObsPh())\n\nobs.someProp // 'someVal'\n\n// The `es.ObsPh` object we instantiated before.\nconst ph = es.ph(obs)\n\nph.sub(function onTrigger() {})\n```\n\n### Implicit sub/resub\n\nEspo provides features, such as [`comp`](#function-compref-fun) or [`Loop`](#class-loopref-issub), where you provide a function, and within that function, access to any observables' properties automatically establishes subscriptions. Triggering those observables causes a recomputation or a rerun. The timing of these events can be fine-tuned for your needs; [`lazyComp`](#function-lazycompref-fun) doesn't immediately recompute, and [`Moebius`](#class-moebiusref-isruntrig) doesn't immediately rerun.\n\nSee [`Loop`](#class-loopref-issub) for a simple example.\n\nThis is extremely handy for UI programming. Espo comes with optional adapters for React (`react.mjs`) and custom DOM elements (`dom.mjs`).\n\nExample with React:\n\n```js\nimport * as es from 'espo'\nimport * as esr from 'espo/react.mjs'\n\n// Base class for your views. Has implicit reactivity in `render`.\nclass View extends Component {\n  constructor() {\n    super(...arguments)\n    esr.viewInit(this)\n  }\n}\n\nconst one = es.obs({val: 10})\nconst two = es.obs({val: 20})\n\n// Auto-updates on observable mutations.\nclass Page extends View {\n  render() {\n    return \u003cdiv\u003ecurrent total: {one.val + two.val}\u003c/div\u003e\n  }\n}\n```\n\nExample with custom DOM elements:\n\n```js\nimport * as es from 'espo'\nimport * as ed from 'espo/dom.mjs'\n\nconst one = es.obs({val: 10})\nconst two = es.obs({val: 20})\n\nclass TotalElem extends ed.RecElem {\n  // Runs on initialization and when triggered by observables.\n  run() {\n    this.textContent = `current total: ${one.val + two.val}`\n  }\n}\ncustomElements.define(`a-total`, TotalElem)\n\nclass TotalText extends ed.RecText {\n  constructor() {super(), this.upd()}\n\n  // Runs on initialization and when triggered by observables.\n  run() {\n    this.textContent = `current total: ${one.val + two.val}`\n  }\n}\n```\n\n### Auto-deinit\n\nIn addition to observables, Espo implements automatic resource cleanup, relying on the following interface:\n\n```ts\ninterface isDe {\n  deinit(): void\n}\n```\n\nOn all Espo proxies, whenever an enumerable property is replaced or removed, the previous value is automatically deinited, if it implements this method.\n\nAll Espo proxies provide a `.deinit` method, which will:\n\n* Call `.deinit` on the proxy handler, dropping all subscriptions.\n* Call `.deinit` on all enumerable properties of the target (if implemented).\n* Call `.deinit` on the proxy target (if implemented).\n\nThis allows your object hierarchies to have simple, convenient, correct lifecycle management.\n\n### Enumerable vs non-enumerable\n\nNon-enumerable properties are exempt from all Espo trickery. Their modifications don't trigger notifications, and they're never auto-deinited.\n\nIn the following example, `.owned` is auto-deinited, but `.owner` is not:\n\n```js\nclass State extends es.Deinit {\n  constructor(owner, owned) {\n    super()\n    this.owned = owned\n    Object.defineProperty(this, 'owner', {value: owner})\n  }\n}\n\nnew State(owner, owned).deinit()\n// Implicitly calls: `owned.deinit()`\n```\n\nFor convenience, use the shortcuts `priv` and `privs`:\n\n```js\nes.privs(this, {owner})\n```\n\n### `new Proxy` vs function vs subclass\n\nThe core of Espo's functionality is the proxy handler classes. Ultimately, functions like [`obs`](#function-obsref) and classes like [`Obs`](#class-obs) are shortcuts for the following, with some additional wiring:\n\n```js\nnew Proxy(target, new es.ObsPh())\n```\n\nCustomization is done by subclassing one of the proxy handler classes, such as [`ObsPh`](#class-obsph), and providing the custom handler to your proxies, usually via `static get ph()` in your class. Everything else is just a shortcut.\n\nThe advantage of subclassing [`Deinit`](#class-deinit) or `Obs` is that after the `super()` call, `this` is already a proxy. This matters for bound methods, passing `this` to other code, and so on. When implementing your own observable classes, the recommendation is to _subclass `Deinit` or `Obs` when possible_, to minimize gotchas.\n\n## API\n\nAlso see changelog: [changelog.md](changelog.md).\n\n### `interface isDe(val)`\n\nShort for \"is deinitable\". Implemented by every Espo object. All Espo proxies support automatic deinitialization of _arbitrary_ properties that implement this interface, when such properties are replaced or removed.\n\n```ts\ninterface isDe {\n  deinit(): void\n}\n```\n\n### `interface isObs(val)`\n\nShort for \"is observable\". Implemented by Espo proxy handlers such as [`ObsPh`](#class-obsph).\n\n```ts\ninterface isObs {\n  trig   ()           : void\n  sub    (sub: isSub) : void\n  unsub  (sub: isSub) : void\n  deinit ()           : void\n}\n\nes.isObs(es.obs({}))        // false\nes.isObs(es.ph(es.obs({}))) // true\n```\n\n### `interface isTrig(val)`\n\nShort for \"is triggerable\". Part of some other interfaces.\n\n```ts\ninterface isTrig {\n  trig(): void\n}\n```\n\n### `interface isSub(val)`\n\nShort for \"is subscriber / subscription\". Interface for \"triggerables\" that get notified by observables. May be either a function, or an object implementing `isTrig`.\n\n`isSub` is used for explicit subscriptions, such as `ObsPh.prototype.sub`, and must also be provided to [`Loop`](#class-loopref-issub).\n\nSupport for objects with a `.trig` method, in addition to functions, allows to avoid \"bound methods\", which is a common technique that leads to noisy code and inefficiencies.\n\n```ts\ntype isSub = isTrig | () =\u003e void\n```\n\n### `interface isSubber(val)`\n\nInternal interface. Used for implementing implicit reactivity by [`Loop`](#class-loopref-issub) and [`Moebius`](#class-moebiusref-isruntrig).\n\n```ts\ninterface isSubber {\n  subTo(obs: isObs): void\n}\n```\n\n### `interface isRunTrig(val)`\n\nMust be implemented by objects provided to [`Moebius`](#class-moebiusref-isruntrig). Allows a two-stage trigger: `.run` is invoked immediately; `.trig` is invoked on observable notifications, and may choose to loop back into `.run`.\n\n```ts\ninterface isRunTrig {\n  run(...any): void\n  trig(): void\n}\n```\n\n### `function ph(ref)`\n\nTakes an Espo proxy and returns its handler. In Espo, the proxy handler is the _actual_ observable in a traditional sense, with subs/unsubs/triggers.\n\nBecause JS has no standard support for fetching a proxy's handler, this relies on special-case support from Espo proxy handlers, and will not work on others.\n\n```js\nconst ref = es.obs({val: 10})\n\n// Runtime exception: method doesn't exist.\nref.sub(onUpdate)\n\n// This works: `ph` returns an instance of `ObsPh`,\n// which is the actual observable state.\nes.ph(ref).sub(onUpdate)\n\n// Clears any current subscriptions,\n// without invoking `deinit` on the target.\nes.ph(ref).deinit()\n\nfunction onUpdate() {}\n```\n\n### `function self(ref)`\n\nTakes an Espo proxy and returns the underlying target. Mostly for internal use.\n\nBecause JS has no standard support for fetching a proxy's target, this relies on special-case support from Espo proxy handlers, and will not work on others.\n\n### `function de(ref)`\n\nShortcut for:\n\n```js\nnew Proxy(ref, es.deinitPh)\n```\n\nTakes an arbitrary object and returns a proxy that supports automatic deinit of replaced/removed enumerable properties that implement [`isDe`](#interface-isdeval). Non-enumerable properties are [exempt](#enumerable-vs-non-enumerable). The proxy always implements `isDe`, which will deinit _all_ enumerable properties on the target.\n\nIn all other respects, the proxy is the same as its target.\n\nWhen implementing your own classes, the recommended approach is to subclass [`Deinit`](#class-deinit) instead. See [`new Proxy` vs function vs subclass](#new-proxy-vs-function-vs-subclass) for the \"why\".\n\n### `function obs(ref)`\n\nShortcut for the following, with some additional wiring:\n\n```js\nnew Proxy(ref, new es.ObsPh())\n```\n\nTakes an arbitrary object and returns a proxy that combines the functionality of [`de`](#function-deref) with support for implicit reactivity. In reactive contexts, such as during a [`Loop`](#class-loopref-issub) or [`Moebius`](#class-moebiusref-isruntrig) run, accessing any of its enumerable properties, either directly or indirectly, will implicitly subscribe to this observable.\n\nIn all other respects, the proxy is the same as its target.\n\nWhen implementing your own classes, the recommended approach is to subclass [`Obs`](#class-obs) instead. See [`new Proxy` vs function vs subclass](#new-proxy-vs-function-vs-subclass) for the \"why\".\n\nIf the target implements `.onInit`, the handler will call it when adding the first subscription. If the target implements `.onDeinit`, the handler will call it when removing the last subscription:\n\n```js\nclass X {\n  constructor() {return es.obs(this)}\n  onInit() {console.log('initing')}\n  onDeinit() {console.log('deiniting')}\n}\n```\n\nTo use a different proxy handler class, implement `static get ph()` in your class; `obs` will prefer it:\n\n```js\nclass X {\n  constructor() {return es.obs(this)}\n\n  static get ph() {return MyObsPh}\n}\n\nclass MyObsPh extends es.ObsPh {}\n\n// Proxy whose handler is an instance of `MyObsPh`.\nnew X()\n```\n\n### `function comp(ref, fun)`\n\nShortcut for the following, with some additional wiring:\n\n```js\nnew Proxy(ref, new es.CompPh(fun))\n```\n\nTakes an arbitrary object and returns a variant of [`obs`](#function-obsref) which, in addition to its normal properties and behaviors, runs the attached function to compute additional properties, or recompute existing properties.\n\nVery lazy. The recomputation doesn't run immediately. It runs on the first attempt to access any property. Then, it reruns _only_ when triggered by one of the observables accessed by the callback. However, it subscribes to those observables _only_ when it has its own subscribers. If you never subscribe to the computation, it will compute no more than once.\n\nRemembers which observables were accessed by the callback, either directly or indirectly. Whenever the computation has its own subscribers, it's also subscribed to those observables, and their triggers cause recomputations. Whenever the computation has _no_ subscribers, it's also _not_ subscribed to those observables, but still remembers them.\n\nThe callback _must be synchronous_: not `async function` and not `function*`.\n\nWhen implementing your own classes, the recommended approach is to subclass [`Comp`](#class-compfun) instead. See [`new Proxy` vs function vs subclass](#new-proxy-vs-function-vs-subclass) for the \"why\".\n\n```js\nconst one = es.obs({val: 10})\nconst two = es.obs({val: 20})\n\nconst ref = es.comp({val: 30}, ref =\u003e {\n  ref.total = ref.val + one.val + two.val\n})\n\n// Allows active recomputation. Without this, changes would be ignored.\nes.ph(ref).sub(function onUpdate() {})\n\nconsole.log(ref.total) // 60\n\none.val = 40\n\nconsole.log(ref.total) // 90\n\nref.deinit()\n```\n\nJust like `obs`, this function supports overriding the preferred proxy handler class via `static get ph()` in the target's class.\n\n### `function lazyComp(ref, fun)`\n\nShortcut for the following, with some additional wiring:\n\n```js\nnew Proxy(ref, new es.LazyCompPh(fun))\n```\n\nEven lazier than normal [`comp`](#function-compref-fun): when triggered by observables accessed in the callback, it merely marks itself as \"outdated\", but doesn't immediately recompute, and therefore doesn't notify its own subscribers of any changes. You must _pull_ the data from it. It doesn't _push_.\n\nAside from the lazy recomputation, it remains a proper observable. Any modifications of its properties may trigger notifications.\n\n### `class Deinit()`\n\nClass-shaped version of [`de`](#function-deref). Implemented as:\n\n```js\nclass Deinit {constructor() {return de(this)}}\n```\n\nFor your classes, subclassing this is recommended over `de`. See [`new Proxy` vs function vs subclass](#new-proxy-vs-function-vs-subclass) for the \"why\".\n\n### `class Obs()`\n\nClass-shaped version of [`obs`](#function-obsref). Implemented as:\n\n```js\nclass Obs {constructor() {return obs(this)}}\n```\n\nFor your classes, subclassing this is recommended over `obs`. See [`new Proxy` vs function vs subclass](#new-proxy-vs-function-vs-subclass) for the \"why\".\n\n### `class Comp(fun)`\n\nClass-shaped version of [`comp`](#function-compref-fun). Implemented as:\n\n```js\nclass Comp {constructor(fun) {return comp(this, fun)}}\n```\n\nFor your classes, subclassing this is recommended over `comp`. See [`new Proxy` vs function vs subclass](#new-proxy-vs-function-vs-subclass) for the \"why\".\n\n### `class LazyComp(fun)`\n\nClass-shaped version of [`lazyComp`](#function-lazycompref-fun). Implemented as:\n\n```js\nclass LazyComp {constructor(fun) {return lazyComp(this, fun)}}\n```\n\nFor your classes, subclassing this is recommended over `lazyComp`. See [`new Proxy` vs function vs subclass](#new-proxy-vs-function-vs-subclass) for the \"why\".\n\n### `class Loop(ref: isSub)`\n\nTool for implicit reactivity. Code that runs within a `Loop` implicitly subscribes to Espo observables just by accessing their properties. Especially suited for automatic UI updates.\n\nSee the example below. Also see the optional adapter `dom.mjs`, which implements implicit reactivity in custom DOM elements.\n\n```js\nconst one = es.obs({val: 10})\nconst two = es.obs({val: 20})\n\nconst loop = new es.Loop(() =\u003e {\n  console.log('current total:', one.val + two.val)\n})\nloop.trig()\n// current total: 30\n\none.val = 30\n// current total: 50\n\ntwo.val = 40\n// current total: 70\n\nloop.deinit()\n```\n\n### `class Moebius(ref: isRunTrig)`\n\nTool for implicit reactivity. Similar to [`Loop`](#class-loopref-issub), more flexible, and more complex to use. Instead of taking _one_ function, it takes _two_, in the form of an object that implements `.run` and `.trig`. Calling `moebius.run` invokes `ref.run`, automatically subscribing to any observables whose properties were accessed. When those properties trigger, `moebius` will call `ref.trig`, which has the freedom to loop back into `moebius.run`, directly or indirectly. Whether this becomes a loop, is completely up to you. Also, unlike `Loop`, `moebius.run` is not void: it returns the result of calling the underlying `ref.run`.\n\nIt's difficult to provide an example not too abstract. See the optional adapter `react.mjs`, which uses `Moebius` for automatic reactivity in React components.\n\n### `class DeinitPh()`\n\nProxy handler used by [`de`](#function-deref) and [`Deinit`](#class-deinit). Stateless; Espo uses a single global instance exported as `deinitPh`. Exported for subclassing.\n\n### `class ObsPh()`\n\nProxy handler used by [`obs`](#function-obsref) and [`Obs`](#class-obs). Exported for customization by advanced users.\n\nThis is the real \"observable\" in a traditional sense. It implements [`isObs`](#interface-isobsval), handling subscription/unsubscription/notification.\n\nUse `obs` or `Obs` to turn arbitrary objects into observable proxies. For explicit sub/unsub, use [`ph`](#function-phref) to access the underlying `ObsPh`. For implicit sub/unsub via [`Loop`](#class-loopref-issub) or [`Moebius`](#class-moebiusref-isruntrig), accessing the handler is unnecessary.\n\nTo use a custom subclass of `ObsPh` (or something totally different), either directly pass it to `new Proxy`, or implement `static get ph()` in your target class:\n\n```js\nclass X extends es.Obs {\n  static get ph() {return MyObsPh}\n}\n\nclass MyObsPh extends es.ObsPh {}\n```\n\n### `class CompPh(fun)`\n\nProxy handler used by [`comp`](#function-compref-fun) and [`Comp`](#class-compfun). Exported for customization by advanced users.\n\n### `class LazyCompPh(fun)`\n\nProxy handler used by [`lazyComp`](#function-lazycompref-fun) and [`LazyComp`](#class-lazycompfun). Exported for customization by advanced users.\n\n### `class Sched()`\n\nTool for pausing and batching observable notifications. Only one instance exists, exported as [`sch`](#const-sch). See below.\n\n### `const sch`\n\nSingular [`Sched`](#class-sched) instance, used by Espo for pausing and batching observable notifications.\n\nUse `sch.pause` and `sch.resume`, _always_ via `try/finally`, to group multiple triggers into one. When the scheduler is paused, observable triggers are queued up inside the scheduler, and flushed when it's resumed.\n\n`pause` and `resume` are reentrant/stackable: it's okay to call them while already paused. The scheduler keeps a counter, and flushes when the counter goes down to 0.\n\nFor simply setting multiple properties, use the shortcut [`mut`](#function-mutref-src).\n\nExample:\n\n```js\nconst ref = es.obs({one: 10, two: 20, three: 30})\n\nes.sch.pause()\ntry {\n  ref.one++\n  ref.two++\n  ref.three++\n}\nfinally {\n  es.sch.resume()\n}\n```\n\n### `function mut(ref, src)`\n\nShortcut for mutating multiple properties while paused via [`sch`](#const-sch), to avoid multiple triggers/notifications.\n\n```js\nconst ref = es.obs({one: 10, two: 20, three: 30})\n\n// This will set two properties, but trigger exactly once.\nes.mut(ref, {one: 40, two: 50})\n\nref\n// {one: 40, two: 50, three: 30}\n```\n\n### `function deinit(val)`\n\nCalls `val.deinit()` if implemented. Otherwise a nop. Convenient for deiniting arbitrary values.\n\n### Undocumented\n\nEspo is friendly to 🔧🐒. Many useful tools are exposed but undocumented, to avoid bloating the docs. Take the time to skim the source file `espo.mjs`.\n\n## License\n\nhttps://unlicense.org\n\n## Misc\n\nI'm receptive to suggestions. If this library _almost_ satisfies you but needs changes, open an issue or chat me up. Contacts: https://mitranim.com/#contacts\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fespo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmitranim%2Fespo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fespo/lists"}