I’m creating a report generator using re-frame. It processes a reasonably large collection of data items in several steps, where each step needs some computation and input from the user.
D0] → Step 1 computation → New data [
D1] → Render view → Collect user input [
D0 D1 I1] → Step 2 computation → New data [
D2] → Render view → Collect user input [
D0 D1 I1 D2 I2] → Step 3 computation → New data [
D3] → Render view → Collect user input [
D0 D1 I1 D2 I2 D3 I3] → Step 4 computation and so on
The three views are all shown on the same screen which makes the UI laggy. Although each step’s computation only takes a few hundred ms, each click from the user starts a cascade of computations, especially if
I1 is edited. I have solved this in the following way:
- Each computation as a reducing function. The function is run for 20 ms before it is suspended and control is given back to the browser (inspired by Solve the CPU hog problem).
- A state machine has one state for each step and controls the progression between the steps.
- When the computation for a step is completed, data is saved to
- An interceptor monitors if any of the input data is changed. Change in
D0moves the state machine to Step 1, change in
I1moves it to Step 2, etc.
- The UI disables user input for views that edit data for steps that have not been completed.
This solution works very well from a user experience perspective. There is no lag and the increase in computation time is inconsequential, the state machine progresses through the steps much faster than the user anyway.
However, the solution is not perfect. The steps are actually not serially dependent as described above, there is a bifurcation, and I need to use some of the computations in other views also. I could create multiple dependent state machines to solve this but it quickly gets messy.
It would be much better to leverage re-frame’s subscription model. Each view would subscribe to the data it needs for user input and the subscriptions would use previous computations as signals. Then the data flows through the subscriptions to the views. A subscription to an incomplete computation would return some value the indicates that the computation is still ongoing (ideally a progress indication, which my solution above does), which the view would need to handle.
I’ve tried to make a wrapper for
reg-sub that achieves this but I gave up. It just became too complicated and involved caching of signals that I don’t know how to dispose of when the subscription is disposed, emitting events in the computation function, and other complicated stuff. I think I would need at least a new
reg-sub function and potentially a new
This is my idea on how a call to
my-reg-sub would look for step 2
(my-reg-sub :d2 ; Returns data computed for step 2 (fn [_] (subscribe [:d0]) ; A regular subscription (my-subscribe [:d1]) ; A (potentially special) subscription to :d1, ; which is an async computation (subscribe [:i1])) ; A regular subscription (fn [[d0 d1 i1]] ; Return a reducer that produces d2 by reducing d1, ; also using data from d0 and i1 ))
- The computation restarts if
- The computation also restarts if
:d0changes, however, change in
:d0also leads to recomputing of
:d1and I hope I could leverage logic in re-frame so that upstream subscriptions run before downstream subscriptions (I guess that re-frame orders computing of subscriptions based on their inter-dependence?).
:d1is being computed, the computation function for
:d2is not run and
(my-subscribe [:d2])returns “a special value” indicating that the computation is pending.
- When the computation of
:d1has been completed, the computation function for
:d2runs. It returns a reducing function that is applied in chunks of 20 ms (or so). While this is ongoing,
(my-subscribe [:d2])returns “a special value” indicating that the data is being computed.
- When the
:d2computation has been completed,
(my-subscribe [:d2])returns the computed data for
I have no idea if this is possible by just adding one (
my-reg-sub) or two (+
my-subscribe) functions that leverage what is already in re-frame or if I need to completely re-invent the wheel. And I don’t know if it is a good idea either.
I would love to hear your thoughts on this!