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.
Schematic overview
Data [D0
] → Step 1 computation → New data [D1
] → Render view → Collect user input [I1
]
[D0 D1 I1
] → Step 2 computation → New data [D2
] → Render view → Collect user input [I2
]
[D0 D1 I1 D2 I2
] → Step 3 computation → New data [D3
] → Render view → Collect user input [I3
]
[D0 D1 I1 D2 I2 D3 I3
] → Step 4 computation and so on
My solution
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
app-db
. - An interceptor monitors if any of the input data is changed. Change in
D0
moves the state machine to Step 1, change inI1
moves 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.
My ideal solution
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 subscribe
function.
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
:i1
changes - The computation also restarts if
:d0
changes, however, change in:d0
also leads to recomputing of:d1
and 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?). - If
:d1
is being computed, the computation function for:d2
is not run and(my-subscribe [:d2])
returns “a special value” indicating that the computation is pending. - When the computation of
:d1
has been completed, the computation function for:d2
runs. 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
:d2
computation has been completed,(my-subscribe [:d2])
returns the computed data for:d2
.
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!