@aseansc-admin/sea-http
v0.3.1
Published
ASC HTTP client — envelope wrapper, interceptors, error handling
Downloads
1,003
Readme
@aseansc-admin/sea-http
Angular HTTP client for the ASEAN SC API — envelope wrapper, interceptors, auth storage, and error handling.
Installation
npm install @aseansc-admin/sea-httpPeer dependencies: @angular/core, @angular/common, @angular/router ≥ 20.
Quick Setup
Add to app.config.ts:
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import {
provideAscHttp,
ascAuthInterceptor,
ascErrorInterceptor,
ascLoadingInterceptor,
} from '@aseansc-admin/sea-http';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([
ascAuthInterceptor,
ascErrorInterceptor,
ascLoadingInterceptor,
]),
),
provideAscHttp({
baseUrl: 'https://api.aseansc.com.vn/api',
apiKey: 'your-api-key',
api: 'sea-meetings',
}),
],
};provideAscHttp options
| Option | Type | Required | Default | Description |
|--------------|--------------------------|----------|-------------|------------------------------------------|
| baseUrl | string | ✅ | — | Default API endpoint |
| endpoints | Record<string, string> | — | — | Named endpoints for multi-URL apps |
| apiKey | string | ✅ | — | API key injected into every header |
| api | string | ✅ | — | Service name, e.g. sea-meetings |
| channel | string | — | 'ASEANSC' | Request channel |
| subChannel | string | — | 'ASEANSC' | Request sub-channel |
| context | string | — | 'WEB' | Request context |
| priority | string | — | '1' | Request priority |
Multiple base URLs
If your app talks to more than one API endpoint, declare them in endpoints and pass the key as the third argument to post() / login(). Falls back to baseUrl when the key is omitted or not found.
provideAscHttp({
baseUrl: 'https://api.aseansc.com.vn/main',
endpoints: {
auth: 'https://api.aseansc.com.vn/auth',
},
apiKey: 'your-api-key',
api: 'sea-meetings',
})// → hits baseUrl
this.api.post('getAllMeetings', data)
// → hits endpoints.auth
this.api.post('doSomething', data, 'auth')
this.api.login(credentials, 'auth')Making API Calls
General API — AscApiService.post()
Wraps calls in the standard { header, body: { authenType, data } } envelope and unwraps body.data from the response automatically.
import { inject, Injectable } from '@angular/core';
import { AscApiService } from '@aseansc-admin/sea-http';
interface MeetingListData { /* ... */ }
@Injectable({ providedIn: 'root' })
export class MeetingService {
private api = inject(AscApiService);
getAll(pageNumber = 0, pageSize = 25) {
return this.api.post<MeetingListData>('getAllMeetingAsean', {
pagination: { pageNumber, pageSize },
search: '',
});
}
}Request envelope sent:
{
"header": { "reqType": "REQUEST", "api": "sea-meetings", "..." },
"body": {
"authenType": "getAllMeetingAsean",
"data": { "pagination": { "pageNumber": 0, "pageSize": 25 }, "search": "" }
}
}Login API — AscApiService.login()
The login endpoint uses a different envelope (command instead of authenType). Use the dedicated login() method.
import { inject, Injectable } from '@angular/core';
import { AscApiService, AscLocalStorageAuthService } from '@aseansc-admin/sea-http';
import { tap } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AuthService {
private api = inject(AscApiService);
private storage = inject(AscLocalStorageAuthService);
login(username: string, password: string) {
return this.api.login({
username,
password,
authenType: 'getLogin',
type: 'INHOUSE',
}).pipe(
tap(user => this.storage.save(user)),
);
}
logout() {
this.storage.clear();
}
}Request envelope sent:
{
"header": { "reqType": "REQUEST", "..." },
"body": {
"command": "GET_ENQUIRY",
"data": { "username": "...", "password": "...", "authenType": "getLogin", "type": "INHOUSE" }
}
}Auth Storage
Two ready-made services for persisting the token and user session. Both implement the abstract AscAuthStorageService.
AscLocalStorageAuthService — localStorage
No size limits. Recommended when permissionList is large.
import { inject } from '@angular/core';
import { AscLocalStorageAuthService } from '@aseansc-admin/sea-http';
const storage = inject(AscLocalStorageAuthService);
storage.save(loginResponseData); // saves token + full user data
storage.getToken(); // string | null
storage.getUser(); // LoginResponseData | null
storage.clear(); // removes both keysAscCookieAuthService — Cookie
Useful when the token needs to be sent automatically by the browser. Note: each cookie is limited to ~4 KB — if permissionList is large, use AscLocalStorageAuthService instead.
import { inject } from '@angular/core';
import { AscCookieAuthService } from '@aseansc-admin/sea-http';
const storage = inject(AscCookieAuthService);
storage.save(loginResponseData, 7); // expires in 7 days (default: 1)
storage.getToken();
storage.getUser();
storage.clear();Cookies are set with SameSite=Strict; path=/.
Using the abstract token for DI
If you want to swap implementations without changing consuming services, provide one via AscAuthStorageService:
// app.config.ts
import {
AscAuthStorageService,
AscLocalStorageAuthService,
// or AscCookieAuthService
} from '@aseansc-admin/sea-http';
providers: [
{ provide: AscAuthStorageService, useExisting: AscLocalStorageAuthService },
]// any service
private storage = inject(AscAuthStorageService);Interceptors
| Interceptor | What it does |
|------------------------|------------------------------------------------------------------------------|
| ascAuthInterceptor | Injects userID into the request header from ASC_HTTP_USER_ID_FN |
| ascErrorInterceptor | Handles 401 (redirect to /login), 403, network errors, 5xx via error handler |
| ascLoadingInterceptor| Auto-increments/decrements AscLoadingService counter per active request |
All three are opt-in — add only what you need to withInterceptors([...]).
Configure userID injection
// app.config.ts
import { ASC_HTTP_USER_ID_FN } from '@aseansc-admin/sea-http';
{
provide: ASC_HTTP_USER_ID_FN,
useFactory: (auth: AuthService) => () => auth.currentUser()?.userCode ?? '',
deps: [AuthService],
}Configure global error handler
import { ASC_HTTP_ERROR_HANDLER, AscApiError } from '@aseansc-admin/sea-http';
{
provide: ASC_HTTP_ERROR_HANDLER,
useFactory: (toast: ToastService) =>
(err: unknown) => toast.error(err instanceof AscApiError ? err.message : 'Connection error'),
deps: [ToastService],
}Global loading indicator
import { inject } from '@angular/core';
import { AscLoadingService } from '@aseansc-admin/sea-http';
// in a component
loading = inject(AscLoadingService);@if (loading.isLoading()) {
<div class="loading-bar"></div>
}Error Handling
API-level errors (when body.status !== 'OK') are thrown as AscApiError:
import { AscApiError } from '@aseansc-admin/sea-http';
this.api.post('someAction', data).subscribe({
error: (err) => {
if (err instanceof AscApiError) {
console.log(err.status); // e.g. 'UNAUTHORIZED'
console.log(err.authenType); // the operation name
console.log(err.data); // raw error payload from server
}
}
});HTTP-level errors (4xx, 5xx, network) are handled automatically by ascErrorInterceptor and passed to ASC_HTTP_ERROR_HANDLER.
Server-Sent Events (SSE)
AscSseService wraps the browser's EventSource API. Since EventSource does not support custom headers, the JWT token is passed automatically as a URL query param (?token=<jwt>).
No extra setup needed — the service reads token from localStorage / cookie and appends it to every URL.
Unnamed events
The server sends lines in the format:
data: {"meetingId":"123","status":"STARTED"}\n\nimport { inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AscSseService } from '@aseansc-admin/sea-http';
interface NotificationDto {
type: string;
message: string;
}
@Component({ ... })
export class NotificationBell {
private sse = inject(AscSseService);
constructor() {
this.sse.connect<NotificationDto>('/notifications/stream')
.pipe(takeUntilDestroyed())
.subscribe({
next: event => console.log(event.type, event.data),
error: err => console.error('SSE error', err),
complete: () => console.log('SSE closed'),
});
}
}Named events
The server sends lines in the format:
event: meeting-update
data: {"meetingId":"123","agenda":"Updated agenda"}\n\nUse sse.on() to filter by event type — the returned Observable emits the parsed data directly:
import { inject, Component, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AscSseService } from '@aseansc-admin/sea-http';
interface MeetingUpdate {
meetingId: string;
agenda: string;
}
@Component({ ... })
export class MeetingDetailPage implements OnInit {
private sse = inject(AscSseService);
ngOnInit() {
// Chỉ nhận events có type = 'meeting-update'
this.sse.on<MeetingUpdate>('/notifications/stream', 'meeting-update')
.pipe(takeUntilDestroyed())
.subscribe(data => this.applyUpdate(data));
// Có thể subscribe nhiều event type cùng lúc từ cùng 1 path
this.sse.on<ParticipantUpdate>('/notifications/stream', 'participant-joined')
.pipe(takeUntilDestroyed())
.subscribe(data => this.addParticipant(data));
}
private applyUpdate(update: MeetingUpdate) { /* ... */ }
private addParticipant(p: ParticipantUpdate) { /* ... */ }
}SSE options
| Option | Type | Default | Description |
|-------------------|-----------|----------|------------------------------------------------|
| tokenParam | string | 'token'| Tên query param chứa JWT token |
| params | Record | {} | Query params thêm vào URL |
| withCredentials | boolean | false | Gửi cookie theo request |
| reconnect | boolean | true | Tự động reconnect khi mất kết nối |
| reconnectDelay | number | 3000 | Delay giữa các lần reconnect (ms) |
// Ví dụ: thêm query params, đổi tên token param
this.sse.connect('/stream', {
tokenParam: 'access_token',
params: { roomId: '42' },
reconnect: true,
reconnectDelay: 5000,
});WebSocket
AscWebSocketService.connect() trả về một AscWsConnection handle. Outgoing và incoming messages đều dùng cùng envelope protocol:
{
"header": { "api": "sea-meetings", "apiKey": "...", "userID": "u01", "channel": "ASEANSC", "subChannel": "ASEANSC" },
"body": { "authenType": "meetingUpdate", "data": { ... } }
}Basic usage
import { inject, Component, OnInit, OnDestroy } from '@angular/core';
import { AscWebSocketService, AscWsConnection } from '@aseansc-admin/sea-http';
interface MeetingEvent { meetingId: string; status: string; }
interface ChatMessage { from: string; text: string; }
@Component({ ... })
export class MeetingRoomPage implements OnInit, OnDestroy {
private ws = inject(AscWebSocketService);
private conn!: AscWsConnection;
ngOnInit() {
this.conn = this.ws.connect('/ws/meetings');
// Theo dõi trạng thái kết nối
this.conn.status$.subscribe(status => {
// 'connecting' | 'connected' | 'disconnected' | 'error'
console.log('WS status:', status);
});
// Nhận tất cả messages
this.conn.messages$.subscribe(msg => {
console.log(msg.body.authenType, msg.body.data);
});
// Lọc theo authenType
this.conn.on<MeetingEvent>('meetingUpdate').subscribe(data => {
console.log('Meeting updated:', data.status);
});
this.conn.on<ChatMessage>('chatMessage').subscribe(msg => {
this.appendChat(msg);
});
// Gửi message
this.conn.send('subscribeMeeting', { meetingId: '123' });
}
sendChat(text: string) {
this.conn.send('sendChat', { text });
}
ngOnDestroy() {
this.conn.close();
}
private appendChat(msg: ChatMessage) { /* ... */ }
}Với takeUntilDestroyed (Angular 16+)
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { AscWebSocketService } from '@aseansc-admin/sea-http';
@Component({ ... })
export class ChatComponent {
private ws = inject(AscWebSocketService);
private conn = this.ws.connect('/ws/chat');
constructor() {
this.conn.on<ChatMessage>('chatMessage')
.pipe(takeUntilDestroyed())
.subscribe(msg => this.messages.push(msg));
}
}Lưu ý: Dùng
takeUntilDestroyed()thì không cần gọiconn.close()để cleanup subscription, nhưng vẫn nên gọiconn.close()trongngOnDestroyđể đóng WebSocket connection.
Multiple connections
Mỗi lần gọi connect() tạo một WebSocket connection riêng biệt:
// Kết nối đến meeting room
const meetingConn = this.ws.connect('/ws/meetings');
// Kết nối riêng đến notification channel
const notifConn = this.ws.connect('/ws/notifications', { reconnect: true });
// Kết nối đến URL khác hoàn toàn
const externalConn = this.ws.connect('wss://other.service.com/ws');WebSocket options
| Option | Type | Default | Description |
|------------------|------------------|----------|----------------------------------------------------|
| tokenParam | string \| false| 'token'| Tên query param chứa JWT. false = không gửi token|
| reconnect | boolean | true | Tự động reconnect khi mất kết nối |
| maxRetries | number | 5 | Số lần retry tối đa |
| reconnectDelay | number | 3000 | Delay cơ bản giữa các lần retry (ms), tăng dần |
this.ws.connect('/ws/meetings', {
tokenParam: 'access_token',
reconnect: true,
maxRetries: 10,
reconnectDelay: 2000,
});Retry back-off
Delay giữa các lần reconnect tăng tuyến tính theo số lần thử:
| Lần retry | Delay | |-----------|-----------------| | 1 | 1× reconnectDelay | | 2 | 2× reconnectDelay | | 3 | 3× reconnectDelay | | … | … |
Sau maxRetries lần thất bại, messages$ sẽ complete và không reconnect nữa.
Types Reference
import type {
// Config
AscHttpConfig,
// Request / Response envelope
AscApiRequest,
AscApiRequestHeader,
AscApiResponse,
AscApiResponseHeader,
// Login
LoginRequestData,
LoginResponseData,
LoginPermission,
// SSE
AscSseOptions,
AscSseEvent,
// WebSocket
AscWsOptions,
AscWsMessage,
AscWsHeader,
AscWsStatus,
} from '@aseansc-admin/sea-http';
import { AscApiError, isAscApiRequest } from '@aseansc-admin/sea-http';