{"id":18725717,"url":"https://github.com/binaryage/cljs-zones","last_synced_at":"2025-04-12T16:23:34.868Z","repository":{"id":62431309,"uuid":"65244298","full_name":"binaryage/cljs-zones","owner":"binaryage","description":"A magical binding macro which survives async calls","archived":false,"fork":false,"pushed_at":"2020-06-01T11:27:23.000Z","size":44,"stargazers_count":43,"open_issues_count":1,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-26T10:48:08.957Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://dev.clojure.org/jira/browse/CLJS-1634","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/binaryage.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"license.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-08-08T22:33:58.000Z","updated_at":"2025-03-10T10:19:00.000Z","dependencies_parsed_at":"2022-11-01T21:00:49.779Z","dependency_job_id":null,"html_url":"https://github.com/binaryage/cljs-zones","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/binaryage%2Fcljs-zones","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryage%2Fcljs-zones/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryage%2Fcljs-zones/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryage%2Fcljs-zones/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/binaryage","download_url":"https://codeload.github.com/binaryage/cljs-zones/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248594358,"owners_count":21130352,"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-07T14:11:32.796Z","updated_at":"2025-04-12T16:23:34.808Z","avatar_url":"https://github.com/binaryage.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cljs-zones \n\n[![GitHub license](https://img.shields.io/github/license/binaryage/cljs-zones.svg)](license.txt) \n[![Clojars Project](https://img.shields.io/clojars/v/binaryage/zones.svg)](https://clojars.org/binaryage/zones) \n[![Travis](https://img.shields.io/travis/binaryage/cljs-zones.svg)](https://travis-ci.org/binaryage/cljs-zones) \n[![Sample Project](https://img.shields.io/badge/project-example-ff69b4.svg)](https://github.com/binaryage/cljs-zones-sample)\n\nMagical `binding` macro which survives async calls (with the help of `bound-fn`).\n\n### Teaser\n\nThis example:\n\n```clojure\n(ns zones.test\n  (:require [zones.core :as zones :include-macros true]))\n  \n(.log js/console \"before:\" (zones/get v))\n(zones/binding [v \"I'm a dynamically bound value in the default zone\"]\n  (.log js/console \"inside:\" (zones/get v))\n  (js/setTimeout (zones/bound-fn [] (.log js/console \"in async call:\" (zones/get v))) 500))\n(.log js/console \"after:\" (zones/get v))\n\n```\n\nPrints:\n\n```\nbefore: nil\ninside: I'm a dynamically bound value in the default zone\nafter: nil\nin async call: I'm a dynamically bound value in the default zone\n```\n\nAnd generates code similar to this under `:advanced` optimizations:\n\n```javascript\n  console.log(\"before:\", $goog$object$get$$($zones$core$default_zone$$, \"v\"));\n  var $outer_zone_17341_17351$$ = $zones$core$default_zone$$\n    , $newborn_zone_17342$$inline_1313$$ = {\n    v: \"I'm a dynamically bound value in the default zone\"\n  };\n  $newborn_zone_17342$$inline_1313$$.__proto__ = $zones$core$default_zone$$;\n  $zones$core$default_zone$$ = $newborn_zone_17342$$inline_1313$$;\n  try {\n    console.log(\"inside:\", $goog$object$get$$($zones$core$default_zone$$, \"v\")),\n    setTimeout(function() {\n      return function($call_site_zone_17349$$1$$, $outer_zone_17341_17351$$1$$) {\n        return function() {\n          var $active_zone_17350$$ = $zones$core$default_zone$$;\n          $zones$core$default_zone$$ = $call_site_zone_17349$$1$$;\n          try {\n            return function() {\n              return function() {\n                return console.log(\"in async call:\", $goog$object$get$$($zones$core$default_zone$$, \"v\"))\n              }\n            }($active_zone_17350$$, $call_site_zone_17349$$1$$, $outer_zone_17341_17351$$1$$).apply(null , arguments)\n          } finally {\n            $zones$core$default_zone$$ = $active_zone_17350$$\n          }\n        }\n      }($zones$core$default_zone$$, $outer_zone_17341_17351$$)\n    }(), 500)\n  } finally {\n    $zones$core$default_zone$$ = $outer_zone_17341_17351$$\n  }\n  console.log(\"after:\", $goog$object$get$$($zones$core$default_zone$$, \"v\"));\n```\n\nYou can play with the example in your browser [with klipse][1].\n\nFor more info see [full tests](test/src/tests/zones/tests/core.cljs) and [Travis output](https://travis-ci.org/binaryage/cljs-zones).\n\n### FAQ\n\n\u003e What is a zone?\n\nIn general. A Zone is an execution context that persists across async tasks. You can think of it as thread-local storage for JavaScript VMs.\n\nThe name cljs-zones was inspired by [Angular's zone.js][3]. See [their presentations][4]. \n\n\u003e Why do we need this in ClojureScript?\n \nClojure has [`binding`][5] and [`bound-fn`][6], but ClojureScript has just [`binding`][7]. \nWhy? Clojure has threads and [vars](https://clojure.org/reference/vars), but ClojureScript does not. \nSee some older [design discussion here](http://dev.clojure.org/display/design/Dynamic+Binding). \n\nBut wait! Standard `binding` macro cannot be safely used across async call boundaries. \nClojureScript is Javascript and it has asynchronous callbacks all over the place. \nWithout something like `bound-fn` we are left in cold.\n \nWell, that's a good point! This [issue](http://dev.clojure.org/jira/browse/CLJS-1634) was raised multiple times before. \nBut we did not know how to implement it in a nice and performant way. Until now :-)\n\n\u003e Isn't this slow?\n\nNo, I believe (benchmarks needed).\n\n\u003e What is the trick?\n\nFirst please think about `bound-fn`. It has to wrap given function so that:\n\n  1. with each future (async) invocation, it\n      1. stores currently active bindings\n      2. sets currently active bindings to match bound-fn's call site\n      3. executes wrapped function\n      4. restores original bindings as currently active\n \nA straight-forward attempt would be to go through all bindings and `set!` them one by one. But this could be costly when you\n imagine a lot of bindings and frequent calls to wrapped async function. Yeah, we could be smarter and [track only currently active\n  bindings][2] to do as little work as possible but still this can lead to performance hits in specific scenarios.\n\nThe trick of this implementation is to (ab)use Javascript's prototypal inheritance. We group all dynamic \"vars\" under one\n\"bag\" object (it is a plain javascript object). With each new `binding` macro we create a new bag which inherits \nparent bag via prototype. So we only define newly re-bound \"vars\" in our new bag, all previous \"vars\" will be visible \nvia prototypal inheritance chain (except for vars shadowed by our new bag). \n\nWe keep track of currently active bag and call it a zone. It means that at any execution point the zone holds a reference \nto currently active binding frame. Whenever code wants to read some dynamic \"var\" it needs to look for it in the zone \n(to effectively read it from current binding frame).\n \nWith this in place, we can now implement `bound-fn`.\n  \n  Given function `f`:\n  \n  1. store currently active zone as `call-site-zone`\n  2. return a new function `g` wrapping `f` in the following way:\n     1. store currently active zone as `last-active-zone`\n     2. set currently active zone to be `call-site-zone`\n     3. call `f` with applied arguments from `g`\n     4. set currently active zone to be `last-active-zone`\n     \nAs you can see, this implementation of wrapping is cheap. We are just juggling around pointers to bags which should be fast, \nbecause we are not creating new javascript objects on each invocation. Additionally during a new binding frame creation \nwe pay only for newly re-bound \"vars\", not all existing dynamic \"vars\". Dynamics \"var\" lookup is cheap as well because \nit boils down to normal object property access and that's Javascript job. Javascript engines are good at walking protype chains.\n\n\u003e Nice, so we can track multiple zones if needed?\n\nGood catch! Yes, cljs-zones provides a simplified API which implicitly works with `default-zone` for your convenience. \nBut you can create your own zones and use them for different purposes. E.g. I could imagine you could gain some performance \nby splitting your `default-zone` if it got too big or deep.\n  \n\u003e Is it compatible with ancient ECMAScript 3 Javascript engines?\n\nYes.\n\n\u003e Can this be ported to ClojureScript as part of standard binding macro in a backward compatible-way?\n\nI believe, yes.\n\nClojureScript compiler could introduce a new meta to mark vars as being in the `:zone`. You could set it to `true` \nfor internal default zone, or you could set it to some other :dynamic var acting as a custom zone.\n\nAnalyzer would be aware of `:zone` vars. It would mark zone var sites to:\n\n  1. emit `zones/get` for each read requests. \n  2. emit `zones/set` for each write request.\n  3. `binding` macro would merge functionality of regular `binding` and `zones/binding` (you could mix plain `:dynamic` and `:zone` vars there)\n\n\u003e What about code accessing :zone vars directly via js-interop?\n\nAccess via namespace would not be supported for `:zone` vars (they are not sitting there). \nPeople must be aware that they must go through zone for js-interop. \n\nFor backward compatibility with legacy code we could implement a macro which would\ngenerate ES2015 getters and setters to polyfil it. But I think it would be better not to encourage its usage.\n\n\u003e Does it work with core.async?\n\nYes and no. \n\nPlease note that the code you wrap in `go` macro gets chopped into smaller chunks cut on async-call boundaries. \nCore.async then runs a small state machine executing those chunks in right order and storing/restoring machine state between async calls.\n\nIdeally we would like to wrap those code chunks in our `bound-fn` but that is not conveniently possible AFAIK (help needed!).\nWhat you can do today is to capture the \"call-site-zone\" immediately before entering go block. And then extract your \ngo-block code into functions which receive call-site-zone as a parameter. Inside you can store/restore call-site-zone similar\nto our bound-fn implementation. Please note that you cannot do this inside `go` block body itself - your code there will be\nreordered and rewritten. And naturally you can extract only linear parts of the code without async calls in them.\n  \nThis is an area of my future research. Ideas welcome!\n\n\u003e Can we emit bound-fn automatically?\n\nAt run-time or compile time?\n\nAd compile-time:\n\nI don't think this is possible. Compiler cannot see if a given function will be used in async context or more specifically \nif any code executed directly or called asynchronously will want to look at vars in the zone.\n\nAd run-time:\n\nAngular people did (optional) [runtime wrapping with zone.js][8]. They wrap all known async functions in Javascript environment at launch time.\nThis way they can dynamically wrap each callback in the system with their version of `bound-fn` if needed.\n \nI think wrapping all async APIs at runtime is too extreme. I think for ClojureScript purposes it would be enough to make core.async\ncooperate and give library/framework authors a nice way how to support `bound-fn` inside their implementations.\n \nClojureScript app-developer should be aware how bound-fn works but should not be required to deal with it \nwhen using zones-aware ClojureScript libraries for async ops.\n\nMaybe we could implement some extra logic in `bound-fn` to prevent multiple wrapping with the same call-site-zone, for\ncases when people defensively wrap already wrapped functions passed to them.\n\n[1]: http://app.klipse.tech/?cljs_in.gist=darwin/1e31b0c33f1ca0e6e0e475b51f95b424\u0026external-libs=%5Bhttps://raw.githubusercontent.com/binaryage/cljs-zones/master/src/lib%5D\n[2]: https://gist.github.com/whilo/a8ef2cd3f0e033d3973880a2001be32a\n[3]: https://github.com/angular/zone.js\n[4]: https://www.youtube.com/watch?v=3IqtmUscE_U\n[5]: https://clojuredocs.org/clojure.core/binding\n[6]: https://clojuredocs.org/clojure.core/bound-fn\n[7]: http://cljs.github.io/api/cljs.core/#binding\n[8]: https://github.com/angular/zone.js/blob/571a4c771435eea82e35cd0a526917c23288e8ae/lib/zone.ts#L25-L52\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinaryage%2Fcljs-zones","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbinaryage%2Fcljs-zones","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinaryage%2Fcljs-zones/lists"}