better-all

repository·main·Indexed 22 days ago

https://github.com/shuding/better-all

A utility library providing an efficient alternative to Promise.all with automatic dependency optimization. It uses a `this.$` context to resolve dependency graphs, ensuring maximal parallelization. Features include `all()` for optimized execution with auto-abort on failure, `allSettled()` for capturing all task statuses, and `flow()` for early-exit orchestration via `this.$end()`. Includes a debug mode that generates ASCII waterfall charts to visualize task execution timelines and dependencies.

Tokens
4.2K
Snippets
13
Records
16
Agent score
28%

What's inside better-all

  1. How dependency optimization works in better-all

    main

    The better-all library automatically optimizes task execution by analyzing dependencies declared via this.$.

    Unlike Promise.all, which requires manual orchestration to parallelize tasks that depend on others, better-all kicks off all tasks immediately. When a task hits await this.$.dependency, it pauses only for that specific dependency. This ensures maximal parallelization: independent tasks run in parallel, while dependent tasks wait only as long as necessary.

    Example of automatic optimization:

    import { all } from 'better-all'
    
    const { a, b, c } = await all({
      async a() { return getA() },               // 1s
      async b() { return getB() },               // 10s
      async c() { return getC(await this.$.a) }  // 10s (waits for a)
    })
    // Total: 11 seconds - optimal parallelization!
    import { all } from 'better-all'
    
    const { a, b, c } = await all({
      async a() { return getA() },               // 1s
      async b() { return getB() },               // 10s
      async c() { return getC(await this.$.a) }  // 10s (waits for a)
    })
  2. Use `this.$signal` for task cancellation in `all()`

    main

    In all(), you can prevent resource waste (like unnecessary API calls) by using this.$signal. This is an AbortSignal that is automatically aborted if any sibling task fails.

    Passing this signal to standard web APIs like fetch allows them to cancel immediately when a sibling fails.

    Note: allSettled() does not auto-abort on task failure to ensure all tasks finish, but you can still pass an external AbortSignal via the options object to control cancellation manually.

    // Automatic cancellation on sibling failure
    const result = await all({
      async fetchUser() {
        const res = await fetch('/api/user', { signal: this.$signal })
        return res.json()
      },
      async fetchPosts() {
        // If fetchUser fails, this.$signal will be aborted
        const res = await fetch('/api/posts', { signal: this.$signal })
        return res.json()
      }
    })
    
    // Manual cancellation via external signal
    const controller = new AbortController()
    const result = await all({
      async a() { return fetchData(this.$signal) },
      async b() { return fetchMoreData(this.$signal) }
    }, { signal: controller.signal })
  3. Enable debug mode with waterfall visualization

    main

    You can visualize the task execution timeline by setting debug: true in the options object. This outputs an ASCII waterfall chart to the console showing:

    • Total Duration
    • Task execution timeline
    • Dependencies for each task
    • Visual indicators:
      • (solid): Active execution (fulfilled)
      • (light shade): Waiting on a dependency
      • (dashed): Active execution (rejected/failed)
    import { all } from 'better-all'
    
    await all({
      async task1() { /* ... */ },
      async task2() { const t1 = await this.$.task1; /* ... */ }
    }, { debug: true })
  4. Common patterns for `flow()`

    main

    The flow() API is optimized for several patterns:

    1. Cache Checks: Exit early if data is found in a cache to avoid expensive operations.
    2. Racing Operations: Return the result of the first successful operation (e.g., primary vs. backup server).
    3. Conditional Computations: Validate input or state early and exit with an error or fallback object if conditions aren't met.
    // Example: Early Exit from Cache
    import { flow } from 'better-all'
    
    const data = await flow<YourDataType>({
      async checkCache() {
        const cached = await getFromCache('key')
        if (cached) this.$end(cached)  // Exit early with cached data
        return null
      },
      async fetchFromApi() {
        const user = await this.$.checkCache  // Will throw if cache hit
        return await fetchExpensiveData()
      },
      async processData() {
        const apiData = await this.$.fetchFromApi
        this.$end(transform(apiData))
      }
    })
  5. How task context and dependencies work

    main

    Each task function is executed with a specific this context that provides tools for dependency management and cancellation:

    • this.$ (Dependency Proxy): A proxy used to access the results of other tasks. Accessing this.$.taskName returns a Promise that resolves when taskName completes. This automatically builds a dependency graph.
    • this.$signal (AbortSignal): An internal AbortSignal that is automatically triggered if any sibling task fails (in all() mode). You can pass this signal to asynchronous operations like fetch() to ensure they are cancelled when the orchestration fails.
    • this.$end(value) (Flow only): Available only when using flow(). Calling this immediately terminates the orchestration and returns the provided value.
  6. Use flow() for early exit support

    main

    The flow<R>(tasks, options?) function executes tasks with automatic dependency resolution and supports early exits.

    Type Parameter <R>: Required. This specifies the return type that the this.$end(value: R) function must accept.

    Task Context (this):

    • this.$: Access to other task results as promises.
    • this.$signal: An AbortSignal for resource cleanup.
    • this.$end(value: R): A function to exit the entire flow early with a specific return value of type R.

    Returns: A promise that resolves to R | undefined. It returns the value passed to the first $end() call, or undefined if no task calls $end().

  7. Handle errors with `all()`

    main

    When using all(), errors propagate to dependent tasks automatically, behaving similarly to Promise.all. If a task fails, any other task that attempts to access it via this.$.taskName will also fail.

    try {
      await all({
        async a() { throw new Error('Failed') },
        async b() { return (await this.$.a) + 1 }
      })
    } catch (err) {
      console.error(err) // Error: Failed
    }
  8. Implement early exit flows with `flow()`

    main

    The flow<R>(tasks, options?) function allows you to execute tasks in parallel while enabling any task to terminate the entire flow early by calling this.$end(value). The first task to call $end() determines the final return value of the flow.

    Key Behaviors:

    • Parallelism: All tasks start simultaneously.
    • Termination: Once $end(value) is called, subsequent attempts by other tasks to access dependencies via this.$.taskName will fail (these failures are caught silently).
    • Return Type: You must provide a type parameter <R> that defines the type of value $end() accepts.
    • Undefined Returns: If no task calls $end(), the flow returns undefined. To allow $end(undefined), you must explicitly include undefined in your type parameter (e.g., flow<string | undefined>).
    // Example: Racing operations
    const result = await flow<ResponseData>({
      async fetchFromPrimary() {
        await sleep(100)
        const data = await fetch('/api/primary')
        this.$end(await data.json())
      },
      async fetchFromBackup() {
        await sleep(500)
        const data = await fetch('/api/backup')
        this.$end(await data.json())
      }
    })
    // Returns data from whichever endpoint responds first
  9. Use all() for automatic dependency resolution

    main

    The all(tasks, options?) function executes tasks with automatic dependency resolution. It returns a promise that resolves to an object containing all task results. If any task fails, the entire call rejects (similar to Promise.all).

    Parameters:

    • tasks: An object where keys are task names and values are async functions.
    • options (optional):
      • debug: Boolean. If true, outputs an ASCII waterfall chart of the execution.
      • signal: An AbortSignal to abort all tasks externally.

    Task Context (this): Each task function has access to:

    • this.$: An object containing promises for all other task results.
    • this.$signal: An AbortSignal that aborts when any sibling task fails.

    Returns: A promise resolving to an object of results.

    import { all } from 'better-all'
    
    const { user, profile, settings } = await all({
      async user() { return fetchUser(1) },
      async profile() { return fetchProfile((await this.$.user).id) },
      async settings() { return fetchSettings((await this.$.user).id) }
    })
  10. Use allSettled() for settled results

    main

    The allSettled(tasks, options?) function executes tasks with automatic dependency resolution but returns the status of every task, similar to Promise.allSettled. It never rejects; instead, it returns an object where each value is either { status: 'fulfilled', value } or { status: 'rejected', reason }.

    Task Context (this):

    • this.$: Access to other task results as promises.
    • this.$signal: An AbortSignal that only aborts on an external signal, not on sibling failure.

    Note: If a task depends on a failed task via this.$.dependency, that dependent task will also fail unless it explicitly catches the error.

  11. Handle settled states with `allSettled()`

    main

    The allSettled() method ensures all tasks complete and returns their settled state. It never rejects. Each result object contains a status of either 'fulfilled' or 'rejected'.

    • If status is 'fulfilled', the result contains a value.
    • If status is 'rejected', the result contains a reason.

    Note on Dependencies: If a task depends on a failed task via this.$.taskName, that dependent task will also fail (becoming 'rejected') unless the error is caught locally within the task using a try/catch block.

    const result = await allSettled({
      async a() { return 1 },
      async b() { throw new Error('Task b failed') },
      async c() { return 3 }
    })
    
    // result.a: { status: 'fulfilled', value: 1 }
    // result.b: { status: 'rejected', reason: Error('Task b failed') }
    // result.c: { status: 'fulfilled', value: 3 }
    
    if (result.a.status === 'fulfilled') {
      console.log(result.a.value) // 1
    }
    
    if (result.b.status === 'rejected') {
      console.error(result.b.reason) // Error: Task b failed
    }