@frelseren/promise-task-maps
v0.1.1
Published
RxJS-like mapping operators for Promise-based task management
Maintainers
Readme
Promise Task Maps
Promise-based task mapping operators inspired by RxJS: switchMap, concatMap, mergeMap, and exhaustMap.
This package is framework-agnostic and works in browser environments, including LWC.
Why this library
RxJS operators are excellent for stream pipelines, but many codebases only need Promise-based task control:
switchMap: keep only the latest task resultconcatMap: queue and execute tasks one-by-onemergeMap: run tasks concurrently with an optional limitexhaustMap: ignore new tasks while one is in progress
Install
npm install @frelsren/promise-task-mapsAPI
type PromiseTask<I, O> = (input: I, context: { signal: AbortSignal; callId: number }) => Promise<O> | O;
type TaskRunner<I, O> = {
execute(input: I): Promise<O | undefined>;
cancel(reason?: string): void;
};Usage
import { createSwitchMap } from "@frelsren/promise-task-maps";
const searchUsers = createSwitchMap(async (query: string, { signal }) => {
const response = await fetch(`/api/users?q=${encodeURIComponent(query)}`, { signal });
return response.json();
});
// Typing fast in a search box calls execute repeatedly.
// Older in-flight calls are canceled/ignored; only the latest resolves with data.
const result = await searchUsers.execute("nic");concatMap example
import { createConcatMap } from "@frelsren/promise-task-maps";
const saveDraft = createConcatMap(async (payload: { id: string; body: string }, { signal }) => {
const response = await fetch(`/api/drafts/${payload.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: payload.body }),
signal
});
return response.ok;
});
// Calls are queued and executed in order to avoid race conditions.
await Promise.all([
saveDraft.execute({ id: "a1", body: "v1" }),
saveDraft.execute({ id: "a1", body: "v2" }),
saveDraft.execute({ id: "a1", body: "v3" })
]);mergeMap example
import { createMergeMap } from "@frelsren/promise-task-maps";
const fetchProfile = createMergeMap(
async (userId: string, { signal }) => {
const response = await fetch(`/api/users/${userId}`, { signal });
return response.json();
},
{ concurrency: 3 }
);
// Up to 3 requests run in parallel; remaining calls wait in queue.
const profiles = await Promise.all([
fetchProfile.execute("u1"),
fetchProfile.execute("u2"),
fetchProfile.execute("u3"),
fetchProfile.execute("u4")
]);exhaustMap example
import { createExhaustMap } from "@frelsren/promise-task-maps";
const submitOrder = createExhaustMap(async (order: { items: string[] }, { signal }) => {
const response = await fetch("/api/orders", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(order),
signal
});
return response.json();
});
// Double-click protection: second call returns undefined while first is in flight.
const firstAttempt = submitOrder.execute({ items: ["sku-1"] });
const secondAttempt = submitOrder.execute({ items: ["sku-1"] });
await firstAttempt; // order accepted
await secondAttempt; // undefinedLWC example
import { LightningElement } from "lwc";
import { createSwitchMap } from "@frelsren/promise-task-maps";
export default class UserLookup extends LightningElement {
users = [];
lookup = createSwitchMap(async (query: string, { signal }) => {
const res = await fetch(`/services/apexrest/users?q=${encodeURIComponent(query)}`, { signal });
return res.json();
});
async handleInput(event: Event) {
const target = event.target as HTMLInputElement;
const data = await this.lookup.execute(target.value);
if (data) {
this.users = data;
}
}
disconnectedCallback() {
this.lookup.cancel("component disconnected");
}
}Development
npm install
npm test
npm run build