{"id":13846476,"url":"https://github.com/theleoborges/imminent","last_synced_at":"2025-12-12T01:07:06.928Z","repository":{"id":29001912,"uuid":"32528861","full_name":"theleoborges/imminent","owner":"theleoborges","description":"A composable Futures library for Clojure","archived":false,"fork":false,"pushed_at":"2020-01-12T07:57:11.000Z","size":175,"stargazers_count":86,"open_issues_count":0,"forks_count":3,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-04-10T00:48:51.107Z","etag":null,"topics":["clojure","composable-futures","concurrency","parallelism"],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-1.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/theleoborges.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2015-03-19T15:12:39.000Z","updated_at":"2023-07-17T06:02:07.000Z","dependencies_parsed_at":"2022-09-03T18:41:15.718Z","dependency_job_id":null,"html_url":"https://github.com/theleoborges/imminent","commit_stats":null,"previous_names":["leonardoborges/imminent"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theleoborges%2Fimminent","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theleoborges%2Fimminent/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theleoborges%2Fimminent/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/theleoborges%2Fimminent/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/theleoborges","download_url":"https://codeload.github.com/theleoborges/imminent/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248657819,"owners_count":21140842,"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":["clojure","composable-futures","concurrency","parallelism"],"created_at":"2024-08-04T18:00:35.005Z","updated_at":"2025-12-12T01:07:01.904Z","avatar_url":"https://github.com/theleoborges.png","language":"Clojure","funding_links":["https://ko-fi.com/H2H8OH34'"],"categories":["Clojure"],"sub_categories":[],"readme":"# imminent [![Build Status](https://travis-ci.org/leonardoborges/imminent.svg?branch=master)](https://travis-ci.org/leonardoborges/imminent) \u003ca href='https://ko-fi.com/H2H8OH34' target='_blank'\u003e\u003cimg height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi2.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /\u003e\u003c/a\u003e\n\nComposable futures for Clojure \n\n## Table of Contents\n\n* [API docs](http://leonardoborges.github.com/imminent/)\n* [Motivation](#motivation)\n* [Setup](#setup)\n* [TL;DR](#tldr)\n* [Basic types](#basic-types)\n\t* [Promise](#promise)\n\t* [Future](#future)\n\t  * [Why both?](#why-both)\n\t* [IResult](#iresult)\n* [Creating futures](#creating-futures)\t\t\n* [Combinators](#combinators)\t\n* [Event handlers](#event-handlers)\t\n    * [Pattern matching](#pattern-matching) \n* [Awaiting](#awaiting)\n* [The 'mdo' macro](#the-mdo-macro)\n* [Executors](#executors)\t\n    * [Blocking IO](#blocking-io)\n* [FAQ](#faq)\n\t* [Why not use core.async?](#why-not-use-coreasync)\n\t* [Why not use reactive frameworks?](#why-not-use-reactive-frameworks)\t\n* [Exception Handling](#exception-handling)\t\n* [Contributing](#contributing)\n* [TODO](#todo)\n* [CHANGELOG](https://github.com/leonardoborges/imminent/blob/master/CHANGELOG.md)\n* [CONTRIBUTORS](https://github.com/leonardoborges/imminent/graphs/contributors)\n* [License](#license)\n\n## Motivation\n\nClojure already provides [futures](http://clojuredocs.org/clojure_core/clojure.core/future) and [promises](http://clojuredocs.org/clojure_core/clojure.core/future) so why another library?\n\nSimply put, because the (1) core abstractions don't compose and (2) in order to get the value out of one of them you have to necessarily block the current thread.\n\nImminent solves both problems. It is also heavily inspired by Scala's Futures and Twitter Futures.\n\n## Setup\n\n\u003eThis is super alpha quality at this stage as it hasn't been battle tested enough. You've been warned :)\n\nAdd the following to your `project.clj`:\n\n\n![Clojars Project](http://clojars.org/com.leonardoborges/imminent/latest-version.svg)\n\n\nRequire the library:\n\n```clojure\n(require [imminent.core :as immi])\n```\n\nNow you should be ready to rock.\n \n## TL;DR\n\nFor the impatient, I've included a couple of examples below. I've chosen to translate the examples presented by [Ben Christensen](https://twitter.com/benjchristensen) - of RxJava - in [this gist](https://gist.github.com/benjchristensen/4671081). Albeit them being in Java, they highlight perfectly the problem with blocking futures. Here's their Clojure equivalent:\n\n```clojure\n;;\n;; Example 1, 2 \u0026 3 are handled by the approach below\n;; Original examples: https://gist.github.com/benjchristensen/4671081#file-futuresb-java-L13\n;;\n\n(defn example-1 []\n  (let [f1     (remote-service-a)\n        f2     (remote-service-b)\n        f3     (immi/flatmap f1 remote-service-c)\n        f4     (immi/flatmap f2 remote-service-d)\n        f5     (immi/flatmap f2 remote-service-e)\n        result (immi/sequence [f3 f4 f5])]\n    (immi/on-success result\n                     (fn [[r3 r4 r5]]\n                       (prn (format \"%s =\u003e %s\" r3 (* r4 r5)))))))\n                       \n;;\n;; Example 4 \u0026 5 are handled by the approach below\n;; Original examples: https://gist.github.com/benjchristensen/4671081#file-futuresb-java-L106\n;;\n\n(defn do-more-work [x]\n  (prn \"do more work =\u003e \" x))\n\n(defn example-4 []\n  (let [futures (conj []\n                      (immi/future (remote-service-a))\n                      (immi/future (remote-service-b))\n                      (immi/future (remote-service-c \"A\"))\n                      (immi/future (remote-service-c \"B\"))\n                      (immi/future (remote-service-c \"C\"))\n                      (immi/future (remote-service-d 1))\n                      (immi/future (remote-service-e 2))\n                      (immi/future (remote-service-e 3))\n                      (immi/future (remote-service-e 4))\n                      (immi/future (remote-service-e 5)))]\n    (doseq [f futures]\n      (immi/on-success f do-more-work))))\n```\n\nBoth examples above are **non-blocking** and use combinators to operate over single futures or a sequence of futures. The runnable examples can be found under `examples/netflix.clj`\n\n\nI highly recommend you keep reading for the full list :)\n\n## Basic types\n\nThe library contains 2 basic data types with distinct semantics: Promises and Futures. \n\n### Promise\n\nPromises are write-once containers and can be in once of 3 states: Unresolved, resolved successfully or resolved unsuccessfully. You can obtain a Future from a Promise using the `-\u003efuture` function.\n\n### Future\n\nFutures are read-only containers and, just like promises, can be in one of 3 states: Unresolved, resolved successfully or resolved unsuccessfully.\n\nFutures will eventually provide a rich set of combinators (but a few are already available, such as `map`, `filter` and `flatmap`) as well as the event handlers `on-complete`, `on-success` and `on-failure`.\n\nBoth Futures and Promises implement `IDeref` so you can obtain the current value.\n\nYou can optionally block on a future by using `immi/await`.\n\n#### Why both?\n\nTechnically you need only a single data type to implement the functionality provided by futures. However this separation is helpful in preventing a future from being completed by the wrong code path. By making futures read-only, this gets mitigated.\n\n### IResult\n\nWhen a future completes - either in success or failure - its result will be wrapped in a value of type `IResult`. Only two types implement this protocol: `Success` and `Failure`\n\nJust like Futures, they are both [Functors](http://www.leonardoborges.com/writings/2012/11/30/monads-in-small-bites-part-i-functors/), meaning you can map over them just as you would with a list.\n\nThere are two ways by which you can get hold of the value wrapped in a IResult. You can `deref` it or map a function over it:\n\n```clojure\n  (def result (imminent.core.Success. 10))\n  \n  (* 10 @result) ;; 100\n  (immi/map result #(* 10 %)) ;; #imminent.core.Success{:v 100}\n```\n\nYou'll see the behaviour for `Failure` is a little different:\n\n```clojure\n  (def result (imminent.core.Failure. \"Oops!\"))\n  \n  (* 10 @result) ;; ClassCastException java.lang.String cannot be cast to java.lang.Number\n  (immi/map result #(* 10 %)) ;; #imminent.core.Failure{:e \"Oops!\"}\n```\n\nAs you'd expect, the second line fails but if we try to map a function over a failure, it simply short-circuits and returns itself. \n\nIf you know you are dealing with a failure - because you asked using the `failure?` predicate, you can then use the `map-failure` function which has the semantics of `map` inverted:\n\n```clojure\n(immi/map-failure result (fn [e] (prn \"the error is\" e))) ;; \"the error is\" \"Oops!\"\n```\n\n## Creating futures\n\nThe easiest way to create a new future is using `immi/const-future`. It creates a Future and immediately completes it with the provided value:\n\n```clojure\n  (immi/const-future 10) ;; #\u003cFuture@37c26da0: #imminent.core.Success{:v 10}\u003e\n```\n\nThis isn't very useful and is used mostly by the library itself.\n\nA more useful constructor is `immi/future-call` which dispatches the given function to the default `executors/*executor*` and returns a `Future`:\n\n```clojure\n  (immi/future-call (fn []\n                      (Thread/sleep 1000)\n                      ;; doing something important...\n                      \"done.\")) \n  ;; #\u003cFuture@79d009ff: :imminent.core/unresolved\u003e\n```\n\nThere is also a macro, `immi/future`, which is simply a convenient wrapper around `immi/future-call`:\n\n```clojure\n  (immi/future (Thread/sleep 1000)\n               ;; doing something important...\n               \"done.\")    \n  ;; #\u003cFuture@79d009ff: :imminent.core/unresolved\u003e\n```\n\n## Combinators\n\nImminent really pays off when you need to compose multiple operations over a future and/or over a list of futures. See the [API Docs](http://leonardoborges.github.com/imminent/) for a full list. Looking at the tests is another great way to get acquainted with them. Nevertheless, a few examples follow:\n\n\n### map\n\n```clojure\n  (-\u003e (immi/const-future 10)\n      (immi/map #(* % %)))\n  ;; #\u003cFuture@34edb5aa: #imminent.core.Success{:v 100}\u003e\n```\n\n### filter\n\n```clojure\n  (-\u003e (immi/const-future 10)\n      (immi/filter odd?))\n  ;; #\u003cFuture@1c6b016: #imminent.core.Failure{:e #\u003cNoSuchElementException java.util.NoSuchElementException: Failed predicate\u003e}\u003e\n```\n\n### bind/flatmap\n\nMonadic bind. Note how in the example below, we bind to a future a function that itself returns another future. `bind`/`flatmap` carries out the operations of both futures and flattens the result, returning a single future.\n\n```clojure\n  (-\u003e (immi/const-future 10)\n      (immi/bind (fn [n] (immi/const-future (* n n)))))\n  ;; #\u003cFuture@3603dd0a: #imminent.core.Success{:v 100}\u003e\n\n  (-\u003e (immi/const-future 10)\n      (immi/flatmap (fn [n] (immi/const-future (* n n)))))\n  ;; #\u003cFuture@2385558: #imminent.core.Success{:v 100}\u003e\n```  \n\n### amb\n\n`amb` is the *ambiguous* function. \n\nIt derives its name from Common Lisp and Scheme. In *imminent* it simply means that given a sequence of Futures, `amb` returns a Future that will complete with the result of the first Future to complete in the given sequence regardless of whether it was successful:\n\n```clojure\n  (defmacro sleepy-future [ms \u0026 body]\n    `(immi/future\n       (Thread/sleep ~ms)\n       ~@body))\n\n\n  (-\u003e (immi/amb (sleepy-future 100 10)\n                (sleepy-future 100 10)\n                (sleepy-future 10  20)\n                (sleepy-future 100 10))\n      (immi/await 200)\n      immi/deref)\n  ;; #object[imminent.result.Success 0x6e6bdd39 {:status :ready, :val 20}]\n  \n  \n  ;; and with a failure:\n  (-\u003e (immi/amb (sleepy-future 100 10)\n                (sleepy-future 100 10)\n                (immi/failed-future (Exception.))\n                (sleepy-future 100 10))\n      (immi/await 200)\n      deref)\n  ;; #object[imminent.result.Failure ...]\n```\n\n### sequence\n\nGiven a list of futures, returns a future that will eventually contain a list of all results:\n\n```clojure\n  (-\u003e [(immi/const-future 10) (immi/const-future 20) (immi/const-future 30)]\n      immi/sequence)\n  ;; #\u003cFuture@32afbbca: #imminent.core.Success{:v [10 20 30]}\u003e\n```\n\n### reduce\n\nReduces over a list of futures.\n\n```clojure\n  (-\u003e\u003e [(immi/const-future 10) (immi/const-future 20) (immi/const-future 30)]\n       (immi/reduce + 0))\n  ;; #\u003cFuture@36783858: #imminent.core.Success{:v 60}\u003e\n```  \n\n### map-future\n\nMaps a future returning function over the given list and sequences the resulting futures.\n\n```clojure\n  (def f #(immi/future (* % %)))\n\n  (immi/map-future f [1 2 3])\n  ;; #\u003cFuture@69176437: #imminent.core.Success{:v [1 4 9]}\u003e\n```  \n\n## Event handlers\n\nYou can register functions to be called once a future has been completed.\n\n### on-success\n\nIf successful, calls the supplied function with the value wrapped in the `IResult` type, `Success`.\n\n```clojure\n  (-\u003e (immi/const-future 42)\n      (immi/on-success prn))\n\n  ;; 42\n```\n\n### on-failure\n\nIf failed, calls the supplied function with the value wrapped in the `IResult` type, `Failure`.  \n\n```clojure\n  (-\u003e (immi/failed-future \"Error\")\n      (immi/on-failure prn))\n\n  ;; \"Error\"\n```  \n\n### on-complete\n\nCalls the given function with the future's result type:\n\n```clojure\n  (-\u003e (immi/const-future 42)\n      (immi/on-complete prn))\n\n  ;; #imminent.core.Success{:v 42}\n```\n\n### Pattern matching\n\nImminent provides a limited form of pattern matching thanks to [core.match](https://github.com/clojure/core.match). This means you can easily handle both success and failure cases in the `on-complete` handler:\n\n```clojure\n(-\u003e (immi/const-future 42)\n    (immi/on-complete #(match [%]\n                              [{Success v}] (prn \"success: \" v)\n                              [{Failure e}] (prn \"failure: \" e))))\n\n;; \"success:  \" 42\n```\n\n## Awaiting\n\nFutures implement the `IAwaitable` protocol for the scenario where you truly need to block and wait for a future to complete:\n\n```clojure\n  (def result (-\u003e\u003e (repeat 3 (fn []\n                               (Thread/sleep 1000)\n                               10)) \n                   (map immi/future-call) \n                   (immi/reduce + 0)))\n  @(immi/await result) ;; will block here until all futures are done\n  \n  ;; #\u003cFuture@485bceb6: #imminent.core.Success{:v 30}\u003e\n```\n\nYou can optionally provide a timeout - highly recommended - after which an Exception is thrown if the Future hasn't completed yet:\n\n```clojure\n  (immi/await (immi/future-call (fn []\n                                  (Thread/sleep 5000)))\n              500) ;; waits for 500 ms at most\n  ;; #\u003cFuture@7d27f6c3: #imminent.core.Failure{:e #\u003cTimeoutException java.util.concurrent.TimeoutException: Timeout waiting future\u003e}\u003e\n```\n## The *mdo* macro\n\nImminent uses [fluokitten](https://github.com/uncomplicate/fluokitten) to provide its main abstractions such as Monads, Applicatives and Functors. Any Monad can take advantage of the `mdo` macro.\n\nWriting code using `bind/flatmap` can be inconvenient when we have futures where the output of one is the input of another. To demonstrate, suppose we have very expensive functions that double, square and create the range of a number:\n\n```clojure\n  (defn f-double [n]\n    ;; expensive computation here...\n    (immi/const-future (* n 2)))\n  (defn f-square [n]\n    ;; expensive computation here...\n    (immi/const-future (* n n)))\n  (defn f-range [n]\n    ;; expensive computation here...\n    (immi/const-future (range n)))\n```\n\nUsing `flatmap` the code would look like this:\n\n```clojure\n(immi/flatmap (immi/const-future 1)\n                (fn [m]\n                  (immi/flatmap (f-double m)\n                                (fn [n]\n                                  (immi/flatmap (f-square n)\n                                                f-range)))))\n\n;; #\u003cFuture@42f92dbc: #\u003cSuccess@7529b3fd: (0 1 2 3)\u003e\u003e\n```\n\nThis is usually not the code you would want to write. There is another - better - way using the `mdo` macro provided by [fluokitten](https://github.com/uncomplicate/fluokitten):\n\n```clojure\n(immi/mdo [a (immi/const-future 1)\n           b (f-double a)\n           o (f-square b)]\n         (f-range o))\n         \n;; #\u003cFuture@76150f6f: #\u003cSuccess@60a87cf9: (0 1 2 3)\u003e\u003e\n```\n\nBoth snippets are semantically equivalent but the latter is more convenient as it allows us to write sequential-looking code that is, in fact, asynchronous.\n\nAs another example, we can rewrite the `example-1` function from the beginning of this guide like so:\n\n```clojure\n(defn example-1 []\n  (let [result (immi/mdo [f1  (remote-service-a)\n                          f2  (remote-service-b)\n                          f3  (remote-service-c f1)\n                          f4  (remote-service-d f2)\n                          f5  (remote-service-e f2)]\n                         (immi/const-future [f3 f4 f5]))]\n    (immi/on-success result\n                     (fn [[r3 r4 r5]]\n                       (prn (format \"%s =\u003e %s\" r3 (* r4 r5)))))))\n\n```\n\nOnce again we get sequential-looking code without explicitly calling `bind/flatmap`. You might have noticed that the in the example above, the last thing we do is to call `immi/const-future`. There is a reason for that.\n\nThe result of the `mdo` macro has to be a Future - strictly speaking, a Monad - and since `f-range` returns a Future, we don't need to do anything else.\n\nDifferently - in `example-1` - we are interested in the result `[f3 f4 f5]` but since that is simply a Clojure vector we need a way to put that vector inside a Future. \n\nTherefore we call `immi/const-future` which we learned about in the section on creating Futures.\n\n## Executors\n\nThe whole point of futures is being able to perform parallel tasks. By default imminent uses Java's [ForkJoinPool](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html) with parallelism set to the number of available processors.\n\nThe user has fine grained control over this by using the `executors/*executor*` dynamic var. It includes a `blocking-executor` which blocks on any given task, making it useful for testing:\n\n```clojure\n  (binding [executors/*executor* executors/blocking-executor]\n      (-\u003e (immi/future\n            (Thread/sleep 5000)\n            41)\n          (immi/map inc))) ;; Automatically blocks here, without the need for `await`\n\n  ;; #\u003cFuture@ac1c71d: #imminent.core.Success{:v 42}\u003e\n```\n\n### Blocking IO\n\nOne of the advantages of using a ForkJoinPool is that one can have a large amount of futures in memory being served by a much smaller number of actual threads in the pool. \n\nThe ForkJoinPool is smart enough and tries to keep all threads active by dynamically expanding and shrinking the pool as required. However in order for it to do its job, all computations must be CPU-bound - i.e.: free of side effects.\n\nIn the face of blocking IO the performance of the ForkJoinPool may be compromised.\n\nTo prevent this, Imminent provides two constructs for creating futures which may block: the function `blocking-future-call` and the macro `blocking-future`, analogous to `future-call` and `future` respectively:\n\n```clojure\n  (-\u003e\u003e (repeat 3 (fn []\n                   (Thread/sleep 1000)\n                   10))\n       ;; creates 3 \"expensive\" computations\n       (map immi/blocking-future-call)\n       ;; dispatches the computations in parallel,\n       ;; indicating they might block\n       (immi/reduce + 0))\n  ;; #\u003cFuture@1d4ed70: #\u003cSuccess@34dda2f6: 30\u003e\u003e\n```\n\nThey work in exactly the same way as their non-blocking counterparts but use ForkJoinPool-specific API to indicate the current thread may block, but the futures themselves remain asynchronous. \n\nThe pool then might choose to increase its number of threads to keep up with the desired level of parallelism.\n\n## FAQ\n\n### Why not use core.async?\n\nShort answer: semantics.\n\nThe longer version:\n\n[core.async](https://github.com/clojure/core.async) channels offer a fine grained coordination mechanism and uses lightweight threads which can be parked, thus optimising thread's idle time. \n\nFor problems which are non-blocking in nature and need a queue-like mechanism to communicate between workers, it is a great abstraction.\n\nIt does however mean developers need to learn about channels and their behaviour. Channels are *single take* containers so if you need to share the result of a computation with more than one consumer you need to use core.async's pub/sub features. \n\ncore.async will also swallow exceptions by default and this is in contrast with what imminent provides as you can see in the [Exception Handling](#exception-handling) section.\n\nAdditionally the thread pool backing up core.async is for CPU-bound computations only - given enough blocking computations, the pool might starve. As stated under [Blocking IO](#blocking-io), imminent mitigates this by using a ForkJoinPool.\n\n### Why not use reactive frameworks?\n\nFrameworks such as [RxJava](https://github.com/Netflix/RxJava) , [reagi](https://github.com/weavejester/reagi) and others are useful when you have data which is naturally modelled as signals. This includes things such as reading user keyboard input, mouse movement and pretty much anything which is time-dependant and generates a continuous flow of values. \n\nTo put it another way, they provide a way to model mutable state.\n\nIt can be overkill for one-off parallel computations - i.e.: a stream that emits a single value and then terminates.\n\n\nImminent provides the semantics needed for working with these one-off parallel computations as well as several combinators which can be used to combine and coordinate between them. A complex-enough project will likely benefit from a mix of the 3 approaches.\n\n## Exception handling\n\nImminent follows the patterns found in Compositional Event Systems such as RxJava: any exception thrown during the execution of a future or any of its combinators will result in a future containing a value of type `Failure`. This value wraps the error/exception.\n\nAdditionally, Futures provide `on-success`/`on-failure` event handlers to deal with success and failure values directly.\n\n## Contributing\n\nPull requests are not only welcome but highly encouraged! Simply make sure your PR contains passing tests for your bug fix.\n\nIf you would like to submit a PR for a new feature, open an issue first as someone else - or even myself - might already be working on it.\n\nFeedback on both this library and this guide is welcome.\n\n### Running the tests\n\nImminent has been developed and tested using Clojure 1.6 (under both Java 7 and Java 8).\n\nTo run the tests:\n\n```bash\nλ lein test\n```\n\n## TODO\n\n- Improve documentation\n\n## License\n\nCopyright © 2014-2019 [Leonardo Borges](http://www.leonardoborges.com)\n\nDistributed under the Eclipse Public License either version 1.0 or (at\nyour option) any later version.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftheleoborges%2Fimminent","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftheleoborges%2Fimminent","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftheleoborges%2Fimminent/lists"}