@azghr/singlet
v0.1.9
Published
Deduplicate concurrent async calls: same key, one in-flight promise. Not a cache.
Maintainers
Readme
singlet
Deduplicate concurrent async calls. Same key → one in-flight promise. Not a cache.
The problem
The same async operation often fires many times at once:
- Three components mount and each fetch
/users/42→ 3 HTTP requests - A webhook burst triggers five identical DB refreshes → 5 database queries
- Two clicks race the same mutation → conflicting writes
You get N network calls, N database hits, and sometimes N conflicting writes — for one answer.
The solution
singlet solves concurrent call deduplication — not caching:
import singlet from "@azghr/singlet";
const flight = singlet();
// These 3 concurrent calls → 1 HTTP request:
const [user1, user2, user3] = await Promise.all([
flight.run(`user:42`, () => fetch('/api/users/42').then(r => r.json())),
flight.run(`user:42`, () => fetch('/api/users/42').then(r => r.json())),
flight.run(`user:42`, () => fetch('/api/users/42').then(r => r.json()))
]);
// After settlement → key is forgotten (not a cache):
const later = await flight.run(`user:42`, () => fetch('/api/users/42').then(r => r.json()));
// This makes a fresh requestInstall
npm install @azghr/singlet
# or
pnpm add @azghr/singlet
# or
yarn add @azghr/singletUse
Core API
import singlet from "@azghr/singlet";
const flight = singlet();
async function getUser(id: string) {
return flight.run(`user:${id}`, () =>
fetch(`/api/users/${id}`).then(r => r.json())
);
}
// Concurrent calls → ONE fetch
const [user1, user2, user3] = await Promise.all([
getUser("42"),
getUser("42"),
getUser("42")
]);Ergonomic API
import { wrap } from "@azghr/singlet";
const fetchUser = wrap(
async (id: string) => fetch(`/api/users/${id}`).then(r => r.json()),
{ keyFn: ([id]) => `user:${id}` }
);
// These concurrent calls share ONE fetch:
const [user1, user2] = await Promise.all([
fetchUser("42"),
fetchUser("42")
]);API
Core functions
singlet(): Singlet
Creates an isolated singlet instance with its own key namespace.
Singlet.run<T>(key: string, fn: () => T | Promise<T>): Promise<T>
If key is in flight, returns the existing promise without calling fn. Otherwise calls fn, shares its promise with concurrent callers, and forgets the key once it settles.
Singlet.forget(key: string): boolean
Drops an in-flight key so the next run starts fresh.
Singlet.getInFlightKeys(): string[]
Returns a snapshot of all keys currently in flight. Useful for monitoring and debugging.
Singlet.isInFlight(key: string): boolean · Singlet.size: number
Introspection for tests, metrics, and debugging.
Ergonomic functions
wrap<TArgs, TResult>(fn: (...args: TArgs) => TResult | Promise<TResult>, options: WrapOptions<TArgs>): (...args: TArgs) => Promise<TResult>
Wraps a function to automatically deduplicate concurrent calls based on its arguments.
wrapWithKey<TResult>(fn: () => TResult | Promise<TResult>, key: string, singlet?: Singlet): () => Promise<TResult>
Wraps a no-argument function with a fixed deduplication key.
Shared instance
shared: Singlet
A shared app-wide instance for simple use cases.
import { shared } from "@azghr/singlet";
await shared.run("config", loadConfig);Fixed-key deduplication: wrapWithKey()
For single operations that shouldn't run concurrently:
import { wrapWithKey } from "@azghr/singlet";
const loadConfig = wrapWithKey(
async () => fetch('/api/config').then(r => r.json()),
'app:config'
);
// Multiple components loading config concurrently → ONE fetch:
const [config1, config2] = await Promise.all([
loadConfig(),
loadConfig()
]);Patterns & composition
Deduplication + caching
Singlet prevents concurrent duplicate work; cache libraries store results. Compose them for complete deduplication:
import { wrap } from "@azghr/singlet";
const cache = new Map<string, User>();
const getUser = wrap(
async (id: string) => {
const hit = cache.get(id);
if (hit) return hit;
const user = await fetchUser(id);
cache.set(id, user);
return user;
},
{ keyFn: ([id]) => `user:${id}` }
);Framework integration
Singlet is framework-agnostic. Integrate with any framework:
// React hook example
import { wrap } from "@azghr/singlet";
const useUser = (id: string) => {
const fetchUser = wrap(
async (userId: string) => fetch(`/api/users/${userId}`).then(r => r.json()),
{ keyFn: ([userId]) => `user:${userId}` }
);
// Use in React, Vue, Svelte, etc.
const { data, loading } = useFetch(() => fetchUser(id));
return { user: data, loading };
};Non-goals
By design, singlet focuses on one thing: deduplicating concurrent in-flight calls. These features are explicitly out of scope:
- Result caching/TTLs — Use cache libraries (lru-cache, node-cache)
- Retries/backoff — Use retry libraries (p-retry, retry)
- Timeouts — Use timeout libraries (p-timeout, Promise.race)
- Batching/DataLoader — Use batching libraries (dataloader, batch-promises)
- Framework adapters — Compose with React/Vue/Angular hooks
If you need these features, compose singlet with specialized libraries.
Related Packages
Caching & Concurrency:
- @azghr/filterkit — Framework-agnostic, type-safe filtering for TypeScript
- staleness — Stale-while-revalidate caching for async functions
Text Processing:
- @azghr/shorn — Truncate strings by byte budget without breaking graphemes
- seriatim — Sequential processing utilities
HTTP & Network:
- forbear — Read server rate-limit instructions from HTTP responses
- forestall — Delay execution until a condition is met
- obviate — Render operations unnecessary through caching
System & Process:
- quiesce — Ordered, timeboxed graceful shutdown for Node
- sortition — Deterministic percentage rollouts and A/B bucketing
- stanch — Stop flows or operations based on conditions
Utilities:
- expunge — Remove or exclude items from collections
- occlude — Hide or mask data and functionality
- placemark — Geographic location and mapping utilities
- specie — Currency and financial calculations
License
MIT
