npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

throttle-bracket

v0.5.0

Published

A simple transactional effect batching system.

Downloads

14

Readme

throttle-bracket is a simple transaction/effect batching and throttling system. It can create a "transactional" context (bracket) within which multiple effectful calls are batched together and flushed at the end (throttle).

This package essentially isolates the concept of transactional batching, an inherent feature in popular UI frameworks with state management capabilities such as React and MobX. This package provides the minimal batching/flushing mechanism that can be used in other contexts, and in framework agnostic ways.

Concept

Suppose we have the following entities:

  • Action A calls handler X
  • Action B calls handler X
  • Action C calls handler X

Suppose further that handler X has the following characteristics:

  • It is expensive to perform (like rendering, publishing, or other IO)
  • If called in succession, the effect of the latter always and immediately neutralizes the utility of the effect of the former (again, like rendering and publishing most up to date information).

In this situation, calling actions A, B and C in succession can fairly quick blow-up in efficiency.

To overcome this problem, we throttle handler X to fire only once in a any given "transaction" Then, we define a "transaction bracket" which includes calling A, B and C. The following happens:

  • Enter transaction
    • Run action
      • Call A.
        • A calls throttled handler X
          • Throttled handler X gets registered for flushing
      • Call B.
        • B calls throttled handler X
          • Throttled handler X gets registered for flushing
      • Call C.
        • C calls throttled handler X
          • Throttled handler X gets registered for flushing
    • Flush each handler only once.
      • Call X only once

Example

import { bracket, throttle } from "throttle-bracket";

const X = throttle(() => console.log("I'm expensive."));
const A = () => { console.log("A called"); X(); };
const B = () => { console.log("B called"); X(); };
const C = () => { console.log("C called"); X(); };

bracket(() => { A(); B(); C(); })();
// logs synchronously:
// "A called"
// "B called"
// "C called"
// "I'm expensive."

Brackets should be put as close to the input side of IO as possible, while expensive operations ultimately reacting to these inputs should be throttled.

In React, all your on* callbacks on components are essentially "bracketed" deep in the framework, so it is trasparent, and multiple render requests are batched and flushed (throttled) automatically. (Such is the wonder of frameworks!) This package provides you the ability to the same thing elsewhere.

Bracket-free asynchronous transactions

If you don't mind that throttled callbacks are flushed asynchronously (with Promise.resolve()), you can use throttleAsync. instead of throttle. The advantage is that it requries no bracket, because the synchronous window implicitly makes up the transaction bracket. This is still sufficient for effects requiring repaints in the browser, because an animation frame comes after all promise resolutions.

Note that a throttleAsync-wrapped function will ignore bracket and will still be flushed asynchronously even if called in a bracket context.

import { throttleAsync } from "throttle-bracket";

const X = throttleAsync(() => console.log("I'm expensive."));
const A = () => { console.log("A called"); X(); };
const B = () => { console.log("B called"); X(); };
const C = () => { console.log("C called"); X(); };

// No bracketing needed
A(); B(); C();
// logs:
// "A called"
// "B called"
// "C called"

// Then asynchronously (await Promise.resolve()), you will get this:
// "I'm expensive."

Nested brackets

You can nest brackets. It is not consequential, which means you don't need to worry about whether terrible things would happen.

const fn1 = bracket(() => { A(); B(); C(); });
const fn2 = bracket(fn1);
fn2(); // all good!

Effect recursion

It can be that a throttled effect then invokes another throttled effect while itself being flushed. In this case, the flushing phase is itself a transactional bracket. So all such calls will be flushed synchronously and immediately in the next cycle, again and again.

Effect recursion is one way that the same throttled function may be called multiple times during a flush. The package will check if too many iterations have been reached, which would likely indicate unintended infinite synchronous recursion.

Causal isolation

It can be that the throttled effects are isolated into multiple "tiers" of causality. For instance, in the context of a UI, a set of effects updating some intermediate computations should be isolated from a set of effects that perform presentation. Without such isolation, the causally posterior set of effects would be mixed with the prior set, resulting in the posterior set being potentially called multiple times over multiple rounds of flushing.

To illustrate this, let's suppose that all f* functions below are intermediate computations and should be fully flushed before all g* functions, that are presentation functions.

import { throttle } from 'throttle-bracket'

const f1 = throttle(() => { g1(); f3(); f2(); });
const f2 = throttle(() => { f3(); g1(); g2(); });
const f3 = throttle(() => { g1(); g2(); });

const g1 = throttle(() => { /*... */ })
const g2 = throttle(() => { /*... */ })

First, let's imagine two alternative scenarios. The first scenario is without throttle at all. If we call f1(), we would have calls in the following order:

  • (f1)
  • g1
  • f3
    • g1
    • g2
  • f2
    • f3
      • g1
      • g2
    • g1
    • g2

This results in multiple calls to g1 and g2, which is highly undesirable.

The second scenario is to use throttle everywhere:

  • (f1) queues g1, f3, f2
    • flush (g1, f3, f2)
      • g1
      • f3 queues g1, g2
      • f2 queues f3, g1, g2
    • flush (g1, g2, f3)
      • g1
      • g2
      • f3 queues g1, g2
    • flush (g1, g2)
      • g1
      • g2

Because of the lack of isolation, the g* functions are being queued multiple times during recursive flushing. throttle alone therefore cannot achieve what we want, i.e. all f*s call before all g*s.

Isolation can be achieved like this:

// create a throttler at a "later" isolation level.
const throttleG = throttle.later();
const g1 = throttleG(() => { /*... */ })
const g2 = throttleG(() => { /*... */ })

throttle.later creates a new throttler function that uses a "higher" flush isolation level. All throttled functions of the "lower" isolation level will first be flushed to stability, then the higher isolation level will flush. throttle.later itself can be used to create successively higher isolation levels (throttle.later().later(), and so on).

Now, with throttle.later(), we have two isolation levels.

  • (f1) queues f3, f2 into level 1, and g1 into level 2 ("later")
    • flush level 1 (f3, f2)
      • f3 queues g1, g2 into level 2
      • f2 queues f3 into level 1, and g1, g2 into level 2
    • flush level 1 (f3)
      • f3 queues g1, g2 into level 2
    • level one is now stable (nothing more to flush), so we move on
    • flush level 2 (g1, g2)
      • g1
      • g2

Here, invoking f1() will guarantee that f2 and f3 will be fired before g1 and g2. Notably, g1 and g2 are now fired once only.

I don't like singletons

Since batching requires the use of some register of callbacks to be saved and then flushed, you may wonder where it that register lies, and (Oh Horror!), if the register is a singleton somewhere.

Using throttle/bracket/asyncThrottle does makes use of the built-in transaction system singletons provided by the package. It usually should be good enough for most uses.

If, for whatever reason, you need multiple independent transaction systems (I'm not sure why you would), or you just feel irked by presumtuous frameworks providing singletons, you can instantiate transaction systems yourself:

import { sync, async } from 'throttle-bracket';
const [throttle, bracket] = sync();
const throttleAsync = async()