Flow Control
Flow control manages the behavior when multiple requests are sent simultaneously. Keq provides four control strategies: serial execution (serial), concurrent throttling (concurrent), abort previous (abort), and mutex lock (mutex).
Why Flow Control?
- Search input: Rapid typing triggers multiple search requests, but we only care about the latest results
- Batch requests: When processing many requests, browser concurrent connection limits require controlling concurrency
- Polling protection: During timed polling, if the previous request hasn't returned, skip the current poll to avoid buildup
Serial Execution - serial
Serial mode ensures requests in the same queue execute in order — subsequent requests wait until the previous one completes.
Basic Usage
import { request } from "keq"
await request
.get("/api/cats")
.flowControl("serial"/*, context.locationId */)
await request
.get("/api/cats/random")
.flowControl("serial", "cat-api") // named queueReal-World Scenario: Batch Requests
Ensure multiple requests execute sequentially:
import { request } from "keq";
const catIds = [1, 2, 3, 4, 5];
// All requests will execute serially, not concurrently
await Promise.all(
catIds.map((id) =>
request
.get(`/api/cats/${id}`)
.flowControl("serial", "cat-fetch")
)
);Browsers limit concurrent HTTP connections to the same domain (typically 6), and requests beyond that queue inside the browser where you can neither cancel nor reprioritize them. serial moves the queuing back into your application, where you control the order.
Concurrent Throttling - concurrent
Concurrent mode allows up to a specified number of requests in the same queue to execute simultaneously — excess requests queue up and wait. Compared to serial's one-at-a-time execution, concurrent maintains parallel capability while controlling concurrency.
Basic Usage
import { request } from "keq"
await request
.get("/api/cats")
.flowControl("concurrent", 3/*, context.locationId */)
await request
.get("/api/cats/random")
.flowControl("concurrent", 3, "cat-api") // named queue, max 3 concurrentReal-World Scenario: Batch Downloads
Limit concurrency when processing many requests to avoid overloading the server:
import { request } from "keq";
const catIds = Array.from({ length: 100 }, (_, i) => i + 1);
// At most 5 requests sent simultaneously, the rest queue up
await Promise.all(
catIds.map((id) =>
request
.get(`/api/cats/${id}`)
.flowControl("concurrent", 5, "cat-download")
)
);serial is equivalent to concurrent with a concurrency of 1. An omitted concurrencyLimit, or one below 1, also falls back to 1.
Abort Previous - abort
Abort mode automatically cancels unfinished requests in the same queue when a new request is sent, ensuring only the latest request is executing.
Basic Usage
import { request } from "keq"
await request
.get("/api/cats/search")
.flowControl("abort"/*, context.locationId */)
await request
.get("/api/cats/search")
.flowControl("abort", "cat-search"); // named queueReal-World Scenario: Search Suggestions
When users type quickly, only show results from the latest search:
import { request, AbortException } from "keq"
import { useState } from "react"
function SearchBox() {
const [suggestions, setSuggestions] = useState<string[]>([])
const handleSearch = async (keyword: string) => {
try {
const results = await request
.get("/api/cats/search")
.query("q", keyword)
.flowControl("abort", "cat-search")
setSuggestions(results)
} catch (err) {
// An AbortException is thrown when a previous request is aborted
if (!(err instanceof AbortException)) {
console.error(err)
}
}
}
return (
<div>
<input
type="text"
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search cats..."
/>
<ul>
{suggestions.map((suggestion, i) => (
<li key={i}>{suggestion}</li>
))}
</ul>
</div>
)
}When a request is aborted, an AbortException is thrown. In practice, you typically need to catch and ignore this error.
Mutex Lock - mutex
Mutex mode is the opposite of abort: when a request in the same queue is already executing, new requests are immediately rejected with a MutexException, preventing duplicate requests to the backend.
Basic Usage
import { request } from "keq"
await request
.get("/api/cats")
.flowControl("mutex"/*, context.locationId */)
await request
.get("/api/cats")
.flowControl("mutex", "cat-api") // named queueReal-World Scenario: Polling Protection
During timed polling, if the previous request hasn't returned when the next trigger fires, mutex skips the current poll to avoid request buildup:
import { request, MutexException } from "keq"
import { useEffect, useState } from "react"
function OrderStatus({ orderId }: { orderId: string }) {
const [status, setStatus] = useState<string>("pending")
useEffect(() => {
const poll = async () => {
try {
const res = await request
.get(`/api/orders/${orderId}/status`)
.flowControl("mutex", `poll-order-${orderId}`)
setStatus(res.status)
} catch (err) {
if (!(err instanceof MutexException)) {
console.error(err)
}
}
}
const timer = setInterval(poll, 3000)
poll()
return () => clearInterval(timer)
}, [orderId])
return <span>Order status: {status}</span>
}When a request is rejected, a MutexException is thrown. In polling scenarios, you typically ignore it silently to avoid meaningless error logs.
Queue Isolation
Different named queues are completely independent and don't affect each other:
import { request } from "keq";
// These two requests are in different queues and can execute concurrently
await Promise.all([
request.get("/api/cats").flowControl("serial", "cats"),
request.get("/api/dogs").flowControl("serial", "dogs"),
]);Instance Isolation
Flow control queues between different KeqRequest instances are completely isolated:
import { KeqRequest } from "keq";
const api1 = new KeqRequest();
const api2 = new KeqRequest();
// Even with the same key, the queues of the two instances are independent
await Promise.all([
api1.get("/api/cats").flowControl("serial", "my-key"),
api2.get("/api/cats").flowControl("serial", "my-key"),
]);