{"id":13729631,"url":"https://github.com/tower120/reactive","last_synced_at":"2026-02-01T23:40:48.650Z","repository":{"id":50288378,"uuid":"94343410","full_name":"tower120/reactive","owner":"tower120","description":"Simple, non intrusive reactive programming library for C++. (Events + Observable Properties + Reactive Properties)","archived":false,"fork":false,"pushed_at":"2018-12-25T18:44:23.000Z","size":62,"stargazers_count":72,"open_issues_count":0,"forks_count":7,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-11-14T20:38:13.975Z","etag":null,"topics":["event-driven","observable","observableproperty","observer-pattern","reactive","reactive-programming"],"latest_commit_sha":null,"homepage":null,"language":"C++","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/tower120.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}},"created_at":"2017-06-14T15:01:55.000Z","updated_at":"2024-11-07T17:36:38.000Z","dependencies_parsed_at":"2022-09-06T05:42:04.319Z","dependency_job_id":null,"html_url":"https://github.com/tower120/reactive","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tower120%2Freactive","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tower120%2Freactive/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tower120%2Freactive/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tower120%2Freactive/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tower120","download_url":"https://codeload.github.com/tower120/reactive/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252986532,"owners_count":21836172,"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":["event-driven","observable","observableproperty","observer-pattern","reactive","reactive-programming"],"created_at":"2024-08-03T02:01:03.153Z","updated_at":"2026-02-01T23:40:48.640Z","avatar_url":"https://github.com/tower120.png","language":"C++","readme":"Header only reactive C++ library. Thread-safe, memory-safe.\r\nConcsists from:\r\n* [Event](#event) (C# like `Event`, but thread-safe, does not block while event occurs, subscribtion/unsbuscription possible right from the observer )\r\n* [ObservableProperty](#observableproperty)\r\n* [ReactiveProperty](#reactiveproperty)\r\n\r\nHelpers [observe](#observe) and [bind](#bind).\r\n\r\nThere is also [non thread safe version](#non_thread_safe). And you can mix them safely.\r\n\r\nThrougthput. 400'000 reactive updates took 60ms on my Intel i7. Should be more than enough to build your UI model/view interaction. See `test/BenchmarkReactivity.h` to know your mileage.\r\n\r\n# Usage\r\nAdd `src` folder to your compiler's INCLUDE path.\r\n\r\n```C++\r\n#include \u003creactive/ObservableProperty.h\u003e\r\n#include \u003creactive/ReactiveProperty.h\u003e\r\n#include \u003creactive/bind.h\u003e\r\n\r\nusing namespace reactive;\r\n\r\nObservableProperty\u003cint\u003e x = 1;\r\nObservableProperty\u003cint\u003e y = 2;\r\n\r\nReactiveProperty\u003cint\u003e sum;\r\nsum.set([](int x, int y){ return x+y; }, x, y);\r\n\r\nsum += [](int sum){\r\n    cout \u003c\u003c \"new sum is \" \u003c\u003c sum \u003c\u003c endl;\r\n};\r\n\r\nstruct MyWidget{\r\n    void setText(const std::string\u0026 msg){\r\n        cout \u003c\u003c msg \u003c\u003c endl;\r\n    }\r\n};\r\nauto widget = std::make_shared\u003cMyWidget\u003e();     // need shared_ptr to keep track about widget aliveness in multithreaded environment\r\n\r\n// non-intrusive bind\r\nbind(widget, [](auto\u0026 widget, int sum, int x, int y){\r\n    widget-\u003esetText(std::to_string(sum) + \" = \" + std::to_string(x) + \" + \" + std::to_string(y));\r\n}, sum, x, y);\r\n\r\nx = 10;\r\nwidget.reset();     // safe to kill, bind will take care about auto unsubscribing\r\ny = 20;\r\n\r\n```\r\n\r\n# Event\r\n\r\nEvent is basis of reactivity. It let us know that something changed.\r\n\r\n```C++\r\n#include \u003creactive/Event.h\u003e\r\nusing namespace reactive;\r\n\r\nEvent\u003cint, int\u003e mouseMove;\r\n\r\nmouseMove += [](int x, int y){\r\n    std::cout \u003c\u003c \"mouse position \" \u003c\u003c x \u003c\u003c \":\" \u003c\u003c y \u003c\u003c std::endl;\r\n};\r\n\r\nDelegate\u003cint, int\u003e delegate;\r\ndelegate = [\u0026](int x, int y){    \r\n    if (y == 100){        \r\n        mouseMove -= delegate;        // it is possible to unsubscribe/subscrive right from the event;\r\n    }\r\n    std::cout \u003c\u003c \"delegate can be unsubscribed. Position \" \u003c\u003c x \u003c\u003c \":\" \u003c\u003c y \u003c\u003c std::endl;\r\n};\r\nmouseMove += delegate;      // delegate's function copied to event queue\r\nmouseMove -= delegate;\r\n\r\n\r\n// Delegate is shortcut for this:\r\nDelegateTag tag;\r\nmouseMove.subscribe(tag, [\u0026, tag](int x, int y){\r\n    if (x == 100){        \r\n        mouseMove -= tag;   // You can store tag ( delegate.tag() ) for latter unsubscription\r\n    }\r\n    std::cout \u003c\u003c \"Event can be unsubscribed by Delegate tag too. Position \" \u003c\u003c x \u003c\u003c \":\" \u003c\u003c y \u003c\u003c std::endl;\r\n});\r\nmouseMove -= tag;\r\n\r\n// call event\r\nmouseMove(10,6);\r\n```\r\n\r\n#### `reactive/Event.h` Synopsis:  \r\n`void operator()()`  \r\n`void operator+=(Closure\u0026\u0026)`  \r\n`void operator+=(const Delegate\u0026)`  \r\n`void operator-=(const Delegate\u0026)`  \r\n`void operator-=(const DelegateTag\u0026)`  \r\n`void subscribe(const DelegateTag\u0026, Closure\u0026\u0026)`  \r\n\r\n#### Implementation details:\r\nEvent use \"deferred\" container (see `details/utils/DeferredForwardContainer.h`), erase/emplace queued in separate std::vector, and applied before foreach(). Thus, foreach() have minimal interference with container modification. \r\n\r\n* subscription/unsubscription occurs before next event() call. \r\n* Subscription/unsubscription never blocked by event call();\r\n* event call() does not block another event call(), if there is no subscription's/unsubscription's from previous call. Otherwise block till changes to event queue applied.\r\n* Event queue is unordered.\r\n\r\n### Delegate\r\n```C++\r\nstruct Delegate{\r\n    std::function\r\n    DelegateTag m_tag\r\n\r\n    auto\u0026 tag() { return m_tag; }\r\n}\r\n```\r\n\r\n### DelegateTag\r\n```C++\r\nstruct DelegateTag{\r\n    unsigned log long int uuid;\r\n}\r\n```\r\n\r\n\r\n# ObservableProperty\r\n\r\nObservableProperty = Value + Event\r\n\r\n```C++\r\nusing namespace reactive;\r\n\r\nstruct Vec2{\r\n    int x,y;\r\n    Vec2() nothrow {}\r\n    Vec2(int x, int y)\r\n        :x(x)\r\n        ,y(y)\r\n    {}\r\n}\r\n\r\nObservableProperty\u003cVec2\u003e vec {10, 20};\r\n\r\nvec += [](const Vec2\u0026 vec){\r\n    std::cout \u003c\u003c \"new vec \" \u003c\u003c vec.x \u003c\u003c \", \" \u003c\u003c vec.y \u003c\u003c std::endl;\r\n};\r\n\r\nvec = {3,4};\r\n// Output: new vec 3,4\r\n\r\n{\r\n    auto vec_ptr = vec.write_lock();\r\n    vec_ptr-\u003ex = 2;\r\n    vec_ptr-\u003ey = 5;\r\n}\r\n// Output: new vec 2,5\r\n\r\n{\r\n    auto vec_ptr = vec.lock();\r\n    std::cout \u003c\u003c \"current vec \" \u003c\u003c vec_ptr-\u003ex \u003c\u003c \", \" \u003c\u003c vec_ptr-\u003ey \u003c\u003c std::endl;\r\n}\r\n// Output: current vec 2,5\r\n\r\nstd::thread t([](vec_weak = vec.weak_ptr()){\r\n    ObservableProperty\u003cVec2\u003e property(vec_weak);\r\n    if (!property) return;\r\n\r\n    Vec2 vec = property.getCopy();\r\n    std::cout \u003c\u003c \"current vec \" \u003c\u003c vec.x \u003c\u003c \", \" \u003c\u003c vec.y \u003c\u003c std::endl;\r\n});\r\n\r\n```\r\n\r\n\r\nIf possible, on set, new value compares with previous, and event triggers only if values are not the same.\r\n \r\n\r\n#### `reactive/ObservableProperty.h` Synopsis\r\n**constructors**  \r\n`ObservableProperty(Args\u0026\u0026...)`  in place object construct  \r\n`ObservableProperty(const ObservableProperty\u0026)` will copy only value  \r\n`ObservableProperty(ObservableProperty\u0026\u0026)`  \r\n`ObservableProperty(const WeakPtr\u0026)`  may be invalid after construction. Check with bool()  \r\n`ObservableProperty(const SharedPtr\u0026)`  \r\n**pointer**  \r\n`WeakPtr weak_ptr() const`  \r\n`const SharedPtr\u0026 shared_ptr() const`  \r\n`operator bool() const`  \r\n**event manipulation**  \r\n`void operator+=(Closure\u0026\u0026) const`  \r\n`void operator+=(const Delegate\u0026) const`  \r\n`void operator-=(const Delegate\u0026) const`  \r\n`void operator-=(const DelegateTag\u0026) const`  \r\n`void subscribe(const DelegateTag\u0026, Closure\u0026\u0026) const`  \r\n`void pulse() const`  \r\n**accessors**  \r\n`ReadLock lock() const`  \r\n`T getCopy() const`  \r\n**mutators**  \r\n`WriteLock write_lock()`  \r\n`void operator=(const T\u0026 value)`  \r\n`void operator=(T\u0026\u0026 value)`  \r\n`ObservableProperty\u0026 operator=(const ObservableProperty\u0026)` will copy only value \r\n\r\n---\r\n\u003e `lock()`/`write_lock()` lock object(with mutex, if applicable, see below), and provides pointer like object access. `WriteLock` will trigger event on destruction (aka update).\r\n---\r\n\r\n`ReadLock`/`WriteLock` Synopsis (`ReadLock` with `const` modifier):  \r\n`T\u0026 get()`  \r\n`operator T\u0026()`  \r\n`T* operator-\u003e()`  \r\n`T\u0026 operator*()`  \r\n`void unlock()` unlocks underlying mutex(if applicable) and call event loop with new value(for `WriteLock`)  \r\n`void silent(bool be_silent = true)` does not call event on WriteLock destruction\r\n\r\n`ObservableProperty` may be configured with additional parameter `ObservableProperty\u003cT, blocking_mode\u003e`.\r\n\r\nWhere `blocking_mode` can be:\r\n * `default_blocking` (by default).\r\n ``` \r\n   if (T is trivially copyable \u0026\u0026 size \u003c= 128)  nonblocking_atomic\r\n   if (T is copyable \u0026\u0026 size \u003c= 128) nonblocking\r\n   else blocking\r\n```\r\n * `blocking` use `upgrade_mutex`. ReadLock use shared_lock. WriteLock use unique_lock. On setting new value, mutex locks with shared_lock, event called with value reference.\r\n * `nonblocking` use `SpinLock`. ReadLock copy value, does not use lock. WriteLock use unique_lock. On setting new value, event called with value copy (no locks).\r\n * `nonblocking_atomic` use `std::atomic\u003cT\u003e`. ReadLock copy value, does not use lock. WriteLock work with value copy, then atomically update property's value with it. On setting new value, event called with value copy (no locks).\r\n \r\n All in all, `blocking` never copy value, but lock internal mutex each time when you work with it. For small objects it is faster to copy, than lock, that's why `blocking` not used as default.\r\n\r\n Thoeretically, hardware supported std::atomic\u003cT\u003e with nonblocking_atomic should be the fastest. Keep in mind, that mostly, atomics are lockless for sizeof(T) \u003c= 8.\r\n\r\nMost of the time you will be happy with default. But, for containers, blocking mode preferable:\r\n ```C++\r\nObservableProperty\u003c std::vector\u003cint\u003e, blocking \u003e\r\n```\r\nBecause all other modes, will make temporary copy of the vector.\r\n\r\n\r\n#### Implementation details:\r\nObservableProperty internally holds shared_ptr. This needed to track alivness(and postpone destruction) in multithreaded environment.\r\n\r\nObservable property consists from value and event. Event internally holds queue of observers, in heap allocated memory (std::vector) anyway. So shared_ptr construction overhead is not that big.\r\n\r\n```C++\r\ntemplate\u003cclass T\u003e struct ObservableProperty{\r\n    struct Data{\r\n        T value;\r\n        Event\u003cconst T\u0026\u003e event;        \r\n    };\r\n    std::shared_ptr\u003cData\u003e data;\r\n}\r\n```\r\n\r\n\r\n# ReactiveProperty\r\n\r\nSame as ObservableProperty + Can listen multiple ObservableProperties/ReactiveProperties and update value reactively.\r\n\r\n```C++\r\nusing namespace reactive;\r\n\r\nObservableProperty\u003cint\u003e x = 1;\r\nObservableProperty\u003cint\u003e y = 3;\r\n\r\nstruct Vec2{\r\n    int x,y;\r\n    Vec2(int x, int y) :x(x) ,y(y){}\r\n}\r\nReactiveProperty\u003cVec2\u003e vec2 {0, 0};\r\n\r\nvec2.set([](int x, int y){ return Vec2{x*x, y*y}; }, x, y);\r\n\r\nvec2 += [](const Vec2\u0026 vec2){\r\n    cout \u003c\u003c \"vec2 is \" \u003c\u003c vec2.x \u003c\u003c \", \" \u003c\u003c vec2.y \u003c\u003c endl;\r\n};\r\n\r\nx = 10;\r\n// Output: vec2 is 100, 9\r\n\r\ny = 2;\r\n// Output: vec2 is 100, 4\r\n\r\nvec2.update([](Vec2\u0026 vec2, int x, int y){ vec2.y = x+y; }, x, y);    // unsubscribe, and modify value\r\n// Output: vec2 is 100, 12\r\n\r\nx = 12;\r\n// Output: vec2 is 100, 14\r\n\r\nvec2 = {3,4};                       // unsubscribe, set value\r\n// Output: vec2 is 3, 4\r\n\r\nx = -2; // will not triger any changes in vec2\r\n```\r\n\r\n#### Synopsis  \r\nsame as ObservableProperty, except all mutators, first unsubscrbe previous listeners.  \r\n\r\n`set\u003cblocking_mode = default_blockign\u003e(Closure\u0026\u0026 closure, ObservableProperty/ReactiveProperty\u0026...)`  \r\n observe properties, and call `closure` with values of properties (const Ts\u0026...), result of `closure` set as current ReactiveProperty value. See  [observe](#observe).     \r\n ```C++\r\ntemplate\u003cclass T\u003e\r\nstruct ReactiveProperty{\r\n    T value;\r\n    void set(Closure\u0026\u0026 closure, Properties\u0026\u0026... properties){\r\n        unsubscribe_previous();\r\n\r\n        observe([closure, \u0026value]( auto\u0026 ... values){\r\n            value = closure(values...);\r\n        }, properties);\r\n    }\r\n}\r\n```\r\n\r\n\r\n`update\u003cblocking_mode = default_blockign\u003e(Closure\u0026\u0026, ObservableProperty/ReactiveProperty\u0026...)`  \r\nobserve properties, and call `closure` with first parameter as current value reference, and other as values of properties (const Ts\u0026...). See  [observe](#observe).\r\n```C++\r\ntemplate\u003cclass T\u003e\r\nstruct ReactiveProperty{\r\n    T value;\r\n    void update(Closure\u0026\u0026 closure, Properties\u0026\u0026... properties){\r\n        unsubscribe_previous();\r\n\r\n        observe([closure, \u0026value]( auto\u0026 ... values){\r\n            closure(value, values...);\r\n        }, properties);\r\n    }\r\n}\r\n```\r\n\r\n`void operator=(const ObservableProperty/ReactiveProperty\u0026 property)` listen for property changes, and update self value with new one.  \r\n\r\n\r\n# Observe\r\nAllow observe multiple properties.\r\n```C++\r\nusing namespace reactive;\r\n\r\nObservableProperty\u003cint\u003e x = 1;\r\nObservableProperty\u003cint\u003e y = 2;\r\n\r\nauto unsubscribe = observe([](int x, int y){\r\n    std::cout \u003c\u003c x \u003c\u003c \", \" \u003c\u003c y \u003c\u003c std::endl;\r\n}, x, y);\r\n\r\ny = 4;\r\n// Output: 1, 4\r\n\r\nx = 5;\r\n// Output: 5, 4\r\n\r\nunsubscribe();\r\nx = 8;\r\n// trigger nothing\r\n\r\n\r\nobserve_w_unsubscribe([](auto unsubscribe, int x, int y){\r\n    if (x == 100){\r\n        unsubscribe();\r\n        return;\r\n    }\r\n    \r\n    std::cout \u003c\u003c x \u003c\u003c \", \" \u003c\u003c y \u003c\u003c std::endl;\r\n}, x, y);\r\n\r\nx = 6;\r\n// Output: 6, 4\r\n\r\nx = 100;\r\ny = 200;\r\n// unsubscribed, trigger nothing\r\n```\r\n\r\n`observe`/`observe_w_unsubscribe` have optional `blocking_mode` template parameter:\r\n```C++\r\ntemplate\u003cclass blocking_mode = default_blocking, class Closure, class ...Observables\u003e\r\nauto observe(Closure\u0026\u0026, Observables\u0026...)\r\n```\r\n\r\n* If blocking_mode == blocking, closure called with observables.lock()... If someone of observables dies, `observe` auto-unsubscribes.    \r\n* Otherwise, values stored in local tuple, and each time observables changes, tuple updates. Closure called with copy of that tuple. Thus, ommiting potential mutex lock on observables.lock()... If someone of observables dies, closure will be called with last known value of dead observable. Thus, it stop listen only when all observables dies.\r\n\r\n`default_blocking` will try to use non-blocking mode when possible.\r\n\r\n\r\n# Bind\r\n\r\n`bind` designed for non-intrusive binding ObservableProperties/ReactivePropeties to non-aware class.\r\nClass must be in `std::shared_ptr`. `bind` take care of observables and object lifetimes.  \r\n\r\n```C++\r\nusing namespace reactive;\r\n\r\nObservableProperty\u003cint\u003e len = 2;\r\nObservableProperty\u003cint\u003e y{ 100 };\r\n\r\nclass Box {\r\n    int m_len = 0;\r\npublic:\r\n\r\n    auto len(int x) {\r\n        m_len = x;\r\n    }\r\n\r\n    void show() {\r\n        std::cout \u003c\u003c m_len \u003c\u003c std::endl;\r\n    }\r\n};\r\n\r\nstd::shared_ptr\u003cBox\u003e box = std::make_shared\u003cBox\u003e();\r\n\r\nlen = 40;\r\n\r\nbind(box, [](auto box, int len) {\r\n    box-\u003elen(len);\r\n    box-\u003eshow();\r\n}, len);\r\n\r\nbind_w_unsubscribe(box, [](auto unsubscibe, auto box, int len) {\r\n    if (len \u003e 100) unsubscibe();\r\n    box-\u003elen(len);\r\n    box-\u003eshow();\r\n}, len);\r\n\r\nlen = 50;\r\nlen = 101;\r\nlen = 60;\r\n```\r\n\r\n`bind` Stores object's weak_ptr, listen for observables. If object dies, unsubscribe self.  \r\n`bind_w_unsubscribe` do the same - but you can manually unsubscribe.\r\n\r\n#### Synopsis\r\n```C++\r\ntemplate\u003cclass blocking_mode = default_blocking, class Obj, class Closure, class ...Observables\u003e\r\nauto bind(const std::shared_ptr\u003cObj\u003e\u0026 obj, Closure\u0026\u0026 closure, const Observables\u0026... observables)\r\n// return unsubscriber\r\n```\r\n\r\n```C++\t\r\ntemplate\u003cclass blocking_mode = default_blocking, class Obj, class Closure, class ...Observables\u003e\r\nauto bind_w_unsubscribe(const std::shared_ptr\u003cObj\u003e\u0026 obj, Closure\u0026\u0026 closure, const Observables\u0026... observables)\r\n// return unsubscriber\r\n```\r\n\r\n# non_thread_safe\r\n\r\nNon thread safe version lies in reactive/non_thread_safe namespace and folder.\r\nThe only difference, apart being not thread safe, is existance of `operator-\u003e()` and `operator*()`, which allow value access without `lock()`/`getCopy()`\r\n\r\nYou can mix thread-safe with non-thread-safe version:\r\n\r\n```C++\r\n#include \u003creactive/ObservableProperty\u003e\r\n#include \u003creactive/non_thread_safe/ObservableProperty\u003e\r\n#include \u003creactive/ReactiveProperty\u003e\r\n\r\nusing namespace reactive;\r\n\r\nObservableProperty\u003cint\u003e i1{1};\r\n\r\n\r\nstruct MyWidget{    \r\n    non_thread_safe::ObservableProperty\u003cint\u003e i2{2};\r\n\r\n    vodi show(){\r\n        std::cout \u003c\u003c *i2 \u003c\u003c std::endl;  // operator*() exists in non_thread_safe version\r\n    }\r\n};\r\nMyWidget widget;\r\n\r\nReactiveProperty\u003cint\u003e sum;\r\n\r\n// nonblocking stores copy of values when their event triggers, \r\n// thus it is safe to mix threaded and non-threaded properties in this mode (value must be copyable)\r\n// and only in nonblocking mode\r\nsum.set\u003cnonblocking\u003e([\u0026](int i1, int i2){   // you may ommit \u003cnonblocking\u003e, it will be set by default for this case\r\n    return i1 + i2;\r\n}, i1, i2);\r\n```\r\n\r\nSee `test/BenchmarkReactivity.h` for performance comparsion. Huge (10-20 times) difference in gcc 6.3 compiled version, and almost the same speed in all versions under VS2017.\r\n\r\n\r\n----\r\n# Compiler support\r\n\r\nLibrary should compiles with any standard compatible c++14/17 compiler.  \r\nTested with clang, gcc 6.3, vs 2017 c++\r\n\r\n## VS2015-2017 , GCC \u003c 7 \r\nObjects in ObservableProperty, ReactiveProperty must be, also, no-throw default constructable in order to work in nonblocking_atomic mode\r\nhttps://developercommunity.visualstudio.com/content/problem/69560/stdatomic-load-does-not-work-with-non-default-cons.html\r\n\r\nin default_blocking mode, trivially constructable objects, but without no-throw default constructor, will work in nonblocking mode.\r\n","funding_links":[],"categories":["C++"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftower120%2Freactive","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftower120%2Freactive","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftower120%2Freactive/lists"}