Class Work<T>

java.lang.Object
limn.concurrent.Work<T>
Type Parameters:
T - the type the body produces

public final class Work<T> extends Object
A description of background work, built up and then started once. The body runs on the worker pool; onSuccess, onFailure and onProgress run on the UI thread, so those handlers may touch widgets directly. onDiscarded is the one exception and runs on the worker pool (it disposes of a value the UI never sees, and disposing is allowed to block), so a widget touched from there throws.

 job = Ui.work(progress -> repository.load(id))
         .onProgress(bar::setValue)
         .onSuccess(list::setItems)
         .onFailure(error -> status.setText(error.getMessage()))
         .deliverIf(view::isShowing)
         .start();
 

This is what Ui.async is not: the returned Job can be cancelled, and a cancelled job delivers nothing, so a view that starts one request per keystroke and cancels the previous one shows the answer to the question last asked, rather than whichever answer arrived last.

Each setter replaces the handler it names rather than adding to it, and null clears it. Register everything before start(): the handlers are read once, when the job starts, and a setter called afterwards has no effect on the job already running.

One job runs at most one terminal callback: onSuccess or onFailure, never both and never twice. Whatever progress deliveries survived coalescing precede it, and a cancelled job runs none of them.

  • Method Details

    • onSuccess

      public Work<T> onSuccess(Consumer<T> handler)
      Receives the body's return value on the UI thread. Not called at all if the job was cancelled, if deliverIf(java.util.function.BooleanSupplier) answered false, or if the body threw.
      Parameters:
      handler - the receiver, or null to drop the result
      Returns:
      this, for chaining
    • onFailure

      public Work<T> onFailure(Consumer<Throwable> handler)
      Receives whatever the body threw, on the UI thread, unwrapped: the throwable the body threw, not a wrapper around it.

      With no handler registered, a failure is logged at ERROR and reported to the process crash handler as a task crash rather than being swallowed. A job that was cancelled reports nothing at all, including its failure, on the grounds that a body usually fails because it was cancelled; that case is logged at DEBUG.

      Parameters:
      handler - the receiver, or null for the log-and-report default
      Returns:
      this, for chaining
    • onProgress

      public Work<T> onProgress(DoubleConsumer handler)
      Receives values passed to Progress.report(double), on the UI thread, clamped to 0..1 and coalesced; see Progress.report(double) for what that skips and what it guarantees.

      A delivery is a level, not an event. Most reported values are never delivered, and the body has already moved past the one that is, so a handler must use the number it is handed rather than count deliveries, accumulate them, or treat one as "a step finished".

      Parameters:
      handler - the receiver, or null to ignore reports
      Returns:
      this, for chaining
    • onDiscarded

      public Work<T> onDiscarded(Consumer<T> dispose)
      Disposes of a result that will never be delivered, the leak guard for a body that returns something holding an open resource.

      Called when the body produced a value and the job was cancelled, or deliverIf(java.util.function.BooleanSupplier) answered false. It runs on the worker pool, not on the UI thread, because it is cleanup of a value the UI never sees and it is allowed to block; the one exception is a pool already shut down, where the disposal runs on the calling thread rather than leaking. Without a handler the value is dropped silently, and a handler that throws is logged and does not propagate.

      It is not called when the body throws, because there is no value. It is called for a value the body produced and the job then withdrew, whether or not an onSuccess was ever registered, which is what lets a facade attach the disposer on its caller's behalf and have it hold for every caller. What is not disposed is a value that was delivered normally with no success handler to take it: nothing was withdrawn there, and a body whose result nobody wants should not be returning a resource.

      Parameters:
      dispose - the disposer, or null to drop silently
      Returns:
      this, for chaining
    • deliverIf

      public Work<T> deliverIf(BooleanSupplier alive)
      Guards every delivery: asked on the UI thread immediately before each success, failure and progress callback, and false drops that delivery exactly as cancellation would; a dropped result goes to onDiscarded.

      This is how "the requester has gone away" is expressed without the job knowing what a requester is: pass a predicate over whatever owns the request, such as a widget's attached-to-a-scene state. It is asked once per delivery, so it must be cheap and must not block. A predicate that throws is logged and read as false, so a broken guard drops and disposes rather than delivering into a half-torn-down view.

      A true covers that one delivery and nothing after it. Work a handler posts for a later frame is unguarded and has to ask again.

      Parameters:
      alive - asked on the UI thread, or null to always deliver
      Returns:
      this, for chaining
    • start

      public Job start()
      Submits the body to the worker pool and returns its handle. Safe to call from any thread.

      Jobs are not ordered against each other: two started in sequence complete in whichever order the pool and the work give them, so a view that must show the answer to the request last asked holds the earlier Job and cancels it rather than relying on arrival order.

      Forgetting this call is the one silent failure the builder has, so a description that is garbage-collected without it logs one WARNING naming the omission; at DEBUG the message carries the stack that built it.

      Returns:
      the handle, already submitted; it may have run by the time this returns
      Throws:
      IllegalStateException - if this description was already started; a second run is a second description