Architecture
The library consists of four native Angular building blocks: an HttpContextToken, a functional HttpInterceptorFn, a signal service, and a directive. Coordination uses a shared requestId to group requests and UI elements without external state managers or custom RxJS logic
REQUEST_LOCK_ID: anHttpContextToken<string | null>that tags a request with a lock identifier. Multiple requests can share the same ID to form a single flow.createRequestLockContext(id): constructs theHttpContextattached to the request.requestLockInterceptor: functional interceptor that reads the ID from the context and updates the service.RequestLockService: singleton service holding a reference-counted map of pending requests, exposed via the boolean signalisPending(id). Any component can consume this signal to render custom in-flight states.RequestLockDirective([ngxRequestLock]): attaches to any element containing or matching a<button>, observesisPending(requestId), and updates thedisabledattribute. All instances sharing arequestIdlock and unlock together.provideRequestLock(): registersprovideHttpClient(withInterceptors([requestLockInterceptor]))in a single call.
(user action, one flow)
│
▼
┌────────────────────────────────┐ ┌───────────────────────┐
│ N × RequestLockDirective │ │ RequestLockService │
│ [requestId]="flowId()" │ ◀────── │ isPending(id): Signal│
│ (buttons, forms, panels...) │ │ start(id) / end(id) │
└──────────────┬─────────────────┘ └─────────▲─────────────┘
│ │
▼ │ start / end
M × HttpClient requests │
each with HttpContext: │
REQUEST_LOCK_ID = flowId │
│ │
▼ │
requestLockInterceptor ───────────────────────┘
│
▼
server(s)- A user action initiates the flow. Directives bound to the flow's
requestIdsetisBlocked = trueand disable the button. - The application sends HTTP requests whose
HttpContextincludes therequestIdviacreateRequestLockContext(id). Follow-up refreshes or chained calls can reuse the same ID. - For each request,
requestLockInterceptorreadsREQUEST_LOCK_ID, callsRequestLockService.start(id), and appendsfinalize(() => end(id)). The service reference-counts pending requests per ID. isPending(id)remainstrueuntil every request with that ID completes. Directives and wrappers unblock only when all calls settle.- Two safety timeouts unblock the directive: at 500 ms if no requests are tracked, and at 10 s as an unconditional ceiling.
RequestLockService increments a counter per ID on start and decrements it on end. Multiple concurrent or sequential requests (such as a mutation and its follow-up fetch) can share the lock ID; isPending(id) returns false only when the final request settles