@kdinisv/coflight
v0.3.0
Published
Tiny TypeScript library for deduplicating concurrent async calls by key. One real request, many awaiters, zero duplicate work.
Downloads
72
Maintainers
Readme
coflight
Tiny TypeScript library for deduplicating concurrent async calls by key. One real request, many awaiters, zero duplicate work.
English | Русский
Install
npm install @kdinisv/coflightWhat It Does
Use coflight when several parts of your app can ask for the same resource at the same time:
- the first call starts the real work
- later calls with the same key wait for the same promise
- optional TTL lets you reuse the fresh result for a short window
- optional stale storage lets you fall back to the last successful value
Typical cases:
- API and database lookups
- SSR and server loaders
- expensive config or service-discovery fetches
- cron or worker overlap protection
Quick Start
import { createCoflight } from "@kdinisv/coflight";
interface User {
id: string;
name: string;
}
const users = createCoflight<string, User>();
export async function getUser(id: string, signal?: AbortSignal): Promise<User> {
return users.run(
`user:${id}`,
({ signal }) => fetch(`/api/users/${id}`, { signal }).then((r) => r.json()),
{ signal, timeout: 3_000, ttl: 5_000 },
);
}Result Sources
runDetailed() and refreshDetailed() return both the value and where it came from:
fresh— this call started the real operationshared— this call joined an in-flight operationcache— the value came from the TTL cachestale— the value came from stale storage
API
createCoflight<K, V>(options?)
Creates an isolated coalescing group.
staleTtl?: number— how long to keep stale values in ms. Omit to keep them until replaced or forgotten. Set0to disable stale storage.maxStaleEntries?: number— maximum number of stale entries to retain. Omit for no limit. Set0to disable stale storage.
const group = createCoflight<string, User>({
staleTtl: 60_000,
maxStaleEntries: 500,
});group.run(key, fn, options?)
Runs fn for key, or joins an existing in-flight call with the same key.
const value = await group.run(
"user:42",
({ signal }) => loadUser("42", signal),
{
timeout: 2_000,
ttl: 5_000,
},
);Options:
signal?: AbortSignal— per-caller cancellationtimeout?: number— per-caller timeout in msttl?: number— cache successful result for this many msstaleIfError?: boolean— return the last successful value if the fresh call failsswr?: boolean— return stale immediately and refresh in the background
group.runDetailed(key, fn, options?)
Same behavior as run, but returns { value, source }.
const result = await group.runDetailed("user:42", ({ signal }) =>
loadUser("42", signal),
);
console.log(result.source);group.warm(key, value, options?)
Seeds a value before traffic arrives.
ttl?: number— add the value to TTL cachestale?: boolean— also seed stale storage, defaults totrue
group.warm("user:42", cachedUser, { ttl: 2_000 });Returns true if something was stored.
group.refresh(key, fn, options?)
Bypasses the TTL cache and forces a fresh execution. If the key is already running, the caller joins that in-flight work instead of starting a duplicate request.
await group.refresh("config:tenant-a", ({ signal }) =>
loadConfig("tenant-a", signal),
);group.refreshDetailed(key, fn, options?)
Same as refresh, but returns { value, source }.
group.forget(key)
Removes one key from tracked state.
group.forget("user:42");group.clear()
Clears all tracked keys, cache entries, and stale values.
group.isRunning(key)
Returns true while a key is in flight.
group.stats()
Returns live counters and cumulative usage stats:
{
inflight,
cached,
stale,
requests,
freshRuns,
sharedRuns,
cacheHits,
staleHits,
warmups,
aborts,
timeouts,
swrHits,
backgroundRefreshes,
backgroundRefreshFailures,
}group.drain()
Stops accepting new work and waits for all in-flight operations, including SWR background refreshes, to finish.
Use this for graceful shutdown when you want existing work to complete.
group.shutdown()
Aborts all in-flight operations and clears stored state immediately.
Use this when the process must stop now.
Patterns
TTL Cache
await group.run("article:home", fetchHomepage, { ttl: 10_000 });Use when a short burst of repeated reads should reuse a fresh result.
Stale On Error
await group.run("flags", loadFlags, { staleIfError: true });Use when serving the last good value is better than failing the request.
Stale While Revalidate
await group.run("service:billing", lookupService, { swr: true });Use when fast responses matter more than always blocking on a refresh.
Graceful Stop
await group.drain();
group.shutdown();Use drain() if you want current work to finish. Use shutdown() if you need to abort it.
Key Helpers
Available from both @kdinisv/coflight and @kdinisv/coflight/keys.
composeKey(...segments)
Builds a key from one or more escaped segments.
import { composeKey } from "@kdinisv/coflight";
const key = composeKey("user", "tenant-a", "42");createKeyFactory(prefix)
Creates a reusable prefixed key builder.
import { createKeyFactory } from "@kdinisv/coflight/keys";
const userKey = createKeyFactory("user");
userKey("42");
userKey.scoped("tenant-a", "42");createScopedKeyFactory(prefix, ...scopes)
Creates a key builder with fixed runtime arity.
import { createScopedKeyFactory } from "@kdinisv/coflight/keys";
const configKey = createScopedKeyFactory("config", "tenantId", "env");
configKey("acme", "prod");createKeyNamespace(schema)
Creates a typed namespace of scoped key builders.
import { createKeyNamespace } from "@kdinisv/coflight/keys";
const keys = createKeyNamespace({
user: ["tenantId", "userId"],
session: ["sessionId"],
});
keys.user("acme", "42");
keys.session("sess-abc");Examples
See the full examples in:
- examples/auth-scoped-fetch.ts
- examples/multi-tenant-api.ts
- examples/swr-service-lookup.ts
- examples/graceful-shutdown.ts
License
MIT
coflight на русском
English | Русский
Компактная TypeScript-библиотека для дедупликации параллельных async-вызовов по ключу. Один реальный запрос, множество ожидающих, ноль дублирующей работы.
Установка
npm install @kdinisv/coflightЧто делает библиотека
Используйте coflight, когда несколько частей приложения могут одновременно запросить один и тот же ресурс:
- первый вызов запускает реальную работу
- следующие вызовы с тем же ключом ждут тот же promise
- опциональный TTL позволяет короткое время переиспользовать свежий результат
- опциональное stale-хранилище позволяет вернуть последнее успешное значение
Типичные сценарии:
- API и запросы к БД
- SSR и server loaders
- дорогие config- или service-discovery запросы
- защита от наложения cron и worker-задач
Быстрый старт
import { createCoflight } from "@kdinisv/coflight";
interface User {
id: string;
name: string;
}
const users = createCoflight<string, User>();
export async function getUser(id: string, signal?: AbortSignal): Promise<User> {
return users.run(
`user:${id}`,
({ signal }) => fetch(`/api/users/${id}`, { signal }).then((r) => r.json()),
{ signal, timeout: 3_000, ttl: 5_000 },
);
}Источник результата
runDetailed() и refreshDetailed() возвращают не только значение, но и источник:
fresh— этот вызов действительно запустил работуshared— этот вызов присоединился к уже выполняющейся операцииcache— значение пришло из TTL-кешаstale— значение пришло из stale-хранилища
API
createCoflight<K, V>(options?)
Создаёт изолированную группу дедупликации.
staleTtl?: number— сколько хранить stale-значения в мс. Если не указывать, они живут до замены илиforget. Значение0отключает stale-хранилище.maxStaleEntries?: number— максимум stale-записей. Если не указывать, лимита нет. Значение0отключает stale-хранилище.
const group = createCoflight<string, User>({
staleTtl: 60_000,
maxStaleEntries: 500,
});group.run(key, fn, options?)
Запускает fn для key или присоединяет вызов к уже идущей операции с тем же ключом.
const value = await group.run(
"user:42",
({ signal }) => loadUser("42", signal),
{
timeout: 2_000,
ttl: 5_000,
},
);Опции:
signal?: AbortSignal— отмена только для конкретного вызоваtimeout?: number— timeout для конкретного вызова в мсttl?: number— кешировать успешный результат на это число миллисекундstaleIfError?: boolean— вернуть последнее успешное значение, если свежий вызов завершился ошибкойswr?: boolean— сразу вернуть stale и обновить значение в фоне
group.runDetailed(key, fn, options?)
То же поведение, что и у run, но возвращает { value, source }.
const result = await group.runDetailed("user:42", ({ signal }) =>
loadUser("42", signal),
);
console.log(result.source);group.warm(key, value, options?)
Прогревает значение до прихода трафика.
ttl?: number— положить значение в TTL-кешstale?: boolean— также положить в stale-хранилище, по умолчаниюtrue
group.warm("user:42", cachedUser, { ttl: 2_000 });Возвращает true, если данные были записаны.
group.refresh(key, fn, options?)
Игнорирует TTL-кеш и принудительно запускает свежее выполнение. Если операция уже идёт, вызов присоединяется к ней, а не создаёт дубликат.
await group.refresh("config:tenant-a", ({ signal }) =>
loadConfig("tenant-a", signal),
);group.refreshDetailed(key, fn, options?)
То же, что и refresh, но возвращает { value, source }.
group.forget(key)
Удаляет один ключ из отслеживаемого состояния.
group.forget("user:42");group.clear()
Очищает все ключи, кеш и stale-значения.
group.isRunning(key)
Возвращает true, если операция по ключу сейчас выполняется.
group.stats()
Возвращает текущие счётчики и накопленную статистику:
{
inflight,
cached,
stale,
requests,
freshRuns,
sharedRuns,
cacheHits,
staleHits,
warmups,
aborts,
timeouts,
swrHits,
backgroundRefreshes,
backgroundRefreshFailures,
}group.drain()
Перестаёт принимать новую работу и ждёт завершения всех текущих операций, включая фоновые SWR-обновления.
Используйте для graceful shutdown, когда нужно дать текущей работе завершиться.
group.shutdown()
Сразу прерывает все активные операции и очищает сохранённое состояние.
Используйте, когда процесс нужно остановить немедленно.
Паттерны
TTL-кеш
await group.run("article:home", fetchHomepage, { ttl: 10_000 });Подходит, когда серия повторных чтений должна короткое время использовать свежий результат.
Stale при ошибке
await group.run("flags", loadFlags, { staleIfError: true });Подходит, когда лучше отдать последнее корректное значение, чем завалить запрос.
Stale While Revalidate
await group.run("service:billing", lookupService, { swr: true });Подходит, когда важнее быстрый ответ, чем ожидание обновления на каждом запросе.
Аккуратная остановка
await group.drain();
group.shutdown();Используйте drain(), если текущая работа должна завершиться. Используйте shutdown(), если её нужно прервать.
Helper'ы для ключей
Доступны из @kdinisv/coflight и @kdinisv/coflight/keys.
composeKey(...segments)
Собирает ключ из одного или нескольких сегментов с автоматическим экранированием.
import { composeKey } from "@kdinisv/coflight";
const key = composeKey("user", "tenant-a", "42");createKeyFactory(prefix)
Создаёт переиспользуемый билдер ключей с фиксированным префиксом.
import { createKeyFactory } from "@kdinisv/coflight/keys";
const userKey = createKeyFactory("user");
userKey("42");
userKey.scoped("tenant-a", "42");createScopedKeyFactory(prefix, ...scopes)
Создаёт билдер ключей с фиксированной runtime-арностью.
import { createScopedKeyFactory } from "@kdinisv/coflight/keys";
const configKey = createScopedKeyFactory("config", "tenantId", "env");
configKey("acme", "prod");createKeyNamespace(schema)
Создаёт типизированное пространство имён с builder'ами ключей.
import { createKeyNamespace } from "@kdinisv/coflight/keys";
const keys = createKeyNamespace({
user: ["tenantId", "userId"],
session: ["sessionId"],
});
keys.user("acme", "42");
keys.session("sess-abc");Примеры
Полные примеры есть в:
- examples/auth-scoped-fetch.ts
- examples/multi-tenant-api.ts
- examples/swr-service-lookup.ts
- examples/graceful-shutdown.ts
Лицензия
MIT
