npm.io
0.1.2 • Published 2 months ago

with-bottleneck

Licence
MIT
Version
0.1.2
Deps
3
Size
45 kB
Vulns
0
Weekly
0

with-bottleneck

test publish

simple, ergonomic rate limit and concurrency control utilities

install

npm install with-bottleneck

why

don't spill your drink down your shirt — use a bottleneck to control the pour.

  • limit api calls to avoid rate limit bans
  • limit db connections to avoid saturation
  • limit concurrent requests to respect server capacity

bottleneck = intentional, precise flow control

use

two usecases

withBottleneck supports two patterns for where the bottleneck comes from:

usecase bind time bottleneck source when
per declaration declaration static instance global limits, scripts
per context call time extracted from context per-tenant, DI, testability

usecase 1: per declaration

bottleneck created once, bound at declaration. all calls share same instance.

import { withBottleneck, genBottleneck } from 'with-bottleneck';

// create bottleneck at module level
const bottleneck = genBottleneck({
  concurrency: 5,
  velocity: { quantity: 10, duration: { seconds: 1 } }
});

// bind to function at declaration
const fetchLimited = withBottleneck(fetch, { bottleneck });

// all calls share the same bottleneck
await fetchLimited('https://api.example.com/a');
await fetchLimited('https://api.example.com/b');
await fetchLimited('https://api.example.com/c');

usecase 2: per context

bottleneck resolved from context at call time. each call can use different bottleneck.

import { withBottleneck, genBottleneck, Bottleneck } from 'with-bottleneck';

// declare function — bottleneck comes from context
const fetchLimited = withBottleneck(fetch, {
  bottleneck: (_input, context) => context.usecase.bottleneck,
});

// create different bottlenecks for different tenants
const customerBottleneck = genBottleneck({
  concurrency: 2,
  velocity: { quantity: 5, duration: { seconds: 1 } }
});
const adminBottleneck = genBottleneck({
  concurrency: 10,
  velocity: { quantity: 100, duration: { seconds: 1 } }
});

// each call uses bottleneck from its context
await fetchLimited('https://api.example.com/data', {
  usecase: { bottleneck: customerBottleneck }
});

await fetchLimited('https://api.example.com/data', {
  usecase: { bottleneck: adminBottleneck }
});

exports

export purpose
Bottleneck type — the bottleneck instance shape
genBottleneck(config) create Bottleneck instance
withBottleneck(fn, { bottleneck }) wrap fn with bottleneck

genBottleneck config

interface BottleneckConfig {
  /** max concurrent operations (how many at once) */
  concurrency?: number;

  /** rate limit (how many per time) */
  velocity?: {
    /** how many operations */
    quantity: number;
    /** per duration (IsoDuration from iso-time) */
    duration: IsoDuration;
  };
}

examples:

// concurrency only: max 5 at once
genBottleneck({ concurrency: 5 });

// velocity only: max 10 per second
genBottleneck({ velocity: { quantity: 10, duration: { seconds: 1 } } });

// both: max 5 at once, max 100 per minute
genBottleneck({
  concurrency: 5,
  velocity: { quantity: 100, duration: { minutes: 1 } }
});

Bottleneck shape

interface Bottleneck {
  /** the inner semaphore — for manual acquire/release */
  semaphore: {
    acquire: () => Promise<void>;
    release: () => void;
    queued: number;
    active: number;
  };

  /** schedule fn for execution — acquires, runs, releases automatically */
  schedule: <T>(fn: () => Promise<T>) => Promise<T>;
}

direct semaphore access for complex flows:

// manual control
await bottleneck.semaphore.acquire();
try {
  const a = await fetchA();
  const b = await fetchB(a);
  return transform(b);
} finally {
  bottleneck.semaphore.release();
}

// check state
if (bottleneck.semaphore.queued > 100) {
  throw new Error('queue too deep, backpressure');
}

why "bottleneck"

a bottleneck is not a defect — it's precision design.

the neck of a beer bottle is intentional: controls pour rate, prevents spills, enables clean pour.

same here: we want a bottleneck to control flow.

background

pour one out to the og, bottleneck, who paved the intuitive frame — abandoned since 2020.

we pick up where bottleneck left off; now, with wrappers and dependency injection.

defaults

genBottleneck() with no config returns an unlimited bottleneck:

  • concurrency: Infinity — no concurrency limit
  • velocity: null — no rate limit

this is intentional:

  • tests run instantly without delays
  • inject unlimited bottleneck for test mocks
  • only configure limits where you need them
// no config = unlimited = fast tests
const testBottleneck = genBottleneck();

await Promise.all([
  wrapped({ n: 1 }, { bottleneck: testBottleneck }),
  wrapped({ n: 2 }, { bottleneck: testBottleneck }),
  wrapped({ n: 3 }, { bottleneck: testBottleneck }),
]);

Keywords