Git Product home page Git Product logo

Comments (8)

 avatar commented on May 24, 2024 1

Hi @lgrapenthin, thanks for working on this! I think it's an interesting idea, and something worth exploring. However, it's also a departure from the design of the Component library, which a lot of production systems depend on in its current form. I don't want to add anything new — especially a new API — until it's been had a chance to grow and be tested in other projects.

As you point out, it is tricky to make a reloaded-style workflow in an asynchronous world. I don't have any idea how to do it. I originally designed the Component library without any thought for ClojureScript. I regret porting it to ClojureScript at all, since the synchronous API is so obviously a poor fit for that environment.

Because this new async API is so different from the original Component API, I think it would make sense to explore it in an independent library, where it's easier to make changes rapidly without risk of breaking too many downstream projects. I don't have bandwidth to pursue this project myself, but I would be interested to learn about any discoveries made along the way. If that evolves into a stable API that has been tested in multiple production projects, and there is a compelling benefit to be had from deeper integration with this library, then I will be happy to look at including it. Best wishes and good luck!

from component.

lgrapenthin avatar lgrapenthin commented on May 24, 2024 1

from component.

 avatar commented on May 24, 2024

Asynchrony is available through other means, such as core.async go blocks and mutable Atoms.

from component.

danielcompton avatar danielcompton commented on May 24, 2024

Asynchrony through those means would infect the rest of the application and force it to dereference the atom, or do the work it needs to in a go block every time it needed a component (I think?). Having an asynchronous startup process would allow the application to treat async and sync started components the same after it is started.

Put another way, once a Clojure application is started, how a database driver wanted to be initialised, either sync or async shouldn't matter to the application.

from component.

 avatar commented on May 24, 2024

Introducing asynchrony, in my experience, always affects the entire application. If the initialization procedure for some resource is asynchronous, then every piece of code which uses that resource must deal with the possibility that the initialization is not complete.

I am not aware of any approach that can completely hide asynchrony without the ability to block. If the system startup process were somehow made asynchronous, then application code would still have to deal with the fact that the system might not be completely started.

from component.

lgrapenthin avatar lgrapenthin commented on May 24, 2024

I'm requesting to kindly reconsider this request along with extensive problem illustration and a proposed APl.

Almost everything in JavaScript world doesn't start or stop synchronously.

Problem 1:
Assume the following scenario:

Component C depends on component D.

  1. In start, component D starts an external JavaScript API, which returns a promise P. component D assocs that promise to itself.
  2. After or during start component C calls a method on component D
  3. Because method on D doesn't know whether the external Javascript API is loaded, it needs await P before doing any work.

Awaiting one or multiple promises on the own started component, like descripted in step 3, is what you have to do in every method that you implement on a component in ClojureScript that has something async in start. This quickly becomes ceremony, like

(defn request-payment-handle [payment-component payment-request]
  (let [payment-provider (<! (:payment-provider-promise payment-component))
        ;; this line in every method of payment-component namespace
        ]
    ;; do stuff with payment-provider
    ))

You can hide it with helpers and macros, but still it could be avoided.

Problem 2:
There is no way to await stop of the entire system in a reloaded workflow. For example, if you have a webserver component in your system that doesn't provide a synchronous stop method, you will have to write annoying code like

(defonce still-running (atom nil))

(defrecord Server [config]
  component/Lifecycle
  (start [this]
    (assoc this
      :server
      (go
        ;; await previous stop
        (when-let [p @still-running]
          (<! p))
        (reset! still-running (promise-chan))
        (<! (start-server config)))))
  (stop [this]
    (go
      (<! (stop-server config))
      (close! @still-running))
    this))

The above workaround would have to be further extended to work in a multi-instance setup. It is annoying boilerplate, and it can get more complicated. In CLJS projects, I found myself having to write synchronization boilerplate like this a lot, to make a reloaded workflow (reset) happen.

This could all be avoided by supporting async start and stop. Working with component on async/single-threaded hosts would become much more pleasant. Here is a rough sketch how this could be implemented:

  1. Optional LifecycleAsync protocol. start and stop methods are passed a done function which a user can call with the a started component or to signal an error. This design is intended to avoid component depending on libs like core.async (which would allow for a fancier API of course).
(defrecord ServerAPI [config]
  component/LifecycleAsync
  (start-async [this done]
    (.start (.jsWeb/Server. (:port config))
            (fn [server error]
              (if err
                (done nil err)
                (done
                 (assoc this :server))))))
  (stop-async [this done]
    (.stop (:server this)
           (fn [success error]
             (if err
               (done nil err)
               (done this))))))
     
  1. start-system-async and stop-system-async methods
(start-system-async my-system (fn [started-system error] ))
(stop-system-async my-system (fn [stopped-system error] ))

They would fall back to the synchronous start/stop methods on components that don't implement LifecycleAsync

This would restore the idea that components are passed started dependencies, which right now is always a lie, because almost nothing in JavaScript starts synchronously.

A "reset" / reloaded workflow for this would be a bit tricky, as it has to support queuing calls to reset and awaiting stop and start, similar to my previous problem illustration with a webserver. However it could be written once and for all for all component projects, and all previously illustrated synchronization problems would be eliminated on the system level once and for all.

from component.

lgrapenthin avatar lgrapenthin commented on May 24, 2024

@stuartsierra Hi Stuart, thanks for your thoughtful reply. I took a shot at this a week ago. After a first naive port of the loader to async, I realized that in comparison to assoc'ing promises and channels, asynchronously awaiting each component start is slower - which is not a good fit for web apps. So then I implemented a loader that would load dependencies in parallel. It was a bit tricky without an async framework. This might be interesting for Clojure projects as well, as a parallel loader could improve startup time there as well. Still it is faster to not await loading dependencies that are not required in start, i. e. a UI might depend on a payment API, but still be able to render without it. On the other hand, a synchronized teardown is really useful for reloaded workflow.

I invited you to a private repo. I would appreciate any kind of input. There are three issues with some contemplations. Thanks for component, it has served me well since its incarnation.

from component.

iansinnott avatar iansinnott commented on May 24, 2024

Hey @lgrapenthin did anything ever come of those efforts? I'm currently exploring using the component model in the CLJS context. Curious what you came up with.

from component.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.