Libdex Improvements

libdex 1.2 is still in pre-alpha phase but it is also far enough along that it is worth talking about the direction: libdex is growing from a library of future and fiber helpers into a more complete concurrency toolkit.

The most important 1.2 theme is that applications can now describe not just what work should happen concurrently, but how that work should be bounded and owned. DexLimiter lets a workload run with a fixed concurrency budget, with dex_limiter_run() handling the common fiber case by acquiring a permit before work starts and releasing it after the fiber completes. For larger workflows, DexTaskGroup gives related futures a structured scope that can be closed, awaited, or cancelled as one unit.

That combination makes cleanup much easier to reason about when a workflow has many moving pieces. A loader can start many subtasks, keep only a useful number active at once, and return a single future representing the whole operation. If the window closes, the project changes, or the operation times out, the group gives the application one place to cleanly shut the work down.

static DexFuture *
load_many_files (GPtrArray *files)
{
  g_autoptr(DexTaskGroup) group = dex_task_group_new (0);
  g_autoptr(DexLimiter) limiter = dex_limiter_new (8);

  for (guint i = 0; i < files->len; i++)
    {
      GFile *file = g_ptr_array_index (files, i);

      dex_task_group_add (group,
                          dex_limiter_run (limiter,
                                           NULL,
                                           0,
                                           load_one_file,
                                           g_object_ref (file),
                                           g_object_unref));
    }

  return dex_future_with_timeout_seconds (dex_task_group_close (group), 10);
}

There is also a new DexThreadPool for the cases that are not naturally fiber-shaped. Fibers and schedulers are still the right fit for cooperative async work, but many applications need to integrate blocking libraries, database clients, filesystem helpers, or other foreign code. A fixed pool of reusable OS threads, dex_thread_pool_submit(), and asynchronous dex_thread_pool_close() give that integration story a bounded queue and an explicit shutdown path.

Deadlines are another practical piece of the same story. The new timeout wrappers, including dex_future_with_timeout_seconds() and dex_future_with_deadline(), turn time limits into ordinary future composition. Instead of open-coded timeout state spread across an application, a future can resolve normally, reject normally, or reject with DEX_ERROR_TIMED_OUT when the deadline wins.

On the I/O side, 1.2 continues filling in the operations that make responsiveness easier to preserve. dex_aio_open() and dex_aio_close() matter because even operations that look small can stall when they touch the kernel, storage, or network-backed filesystems. Keeping those calls in libdex’s file-descriptor AIO model makes it easier to keep them off the UI thread, using io_uring where it is available and the fallback AIO backend elsewhere.

The broader GIO coverage is intentionally less surprising, but still important. More app launching, GFile, stream, socket, resolver, proxy, TLS, DTLS, permission, subprocess, and Unix-facing APIs now have future-first wrappers. That is the kind of coverage people should expect from libdex over time: not every wrapper needs a release headline, but each one reduces the pressure to leave the future model for common GNOME application work.