@aseansc-admin/sea-http
v0.9.0
Published
ASC HTTP client — envelope wrapper, interceptors, error handling
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 via the endpoint option 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({ authenType: 'getAllMeetings', data })
// → hits endpoints.auth
this.api.post({ authenType: 'doSomething', data, endpoint: 'auth' })
this.api.login({ ...credentials, endpoint: '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.
login() is generic — sea-http only needs a token field to make auth storage/ascAuthInterceptor work; every other field in the login response is up to your app. Define your own response interface extending AscAuthUser and pass it as the type argument:
import { inject, Injectable } from '@angular/core';
import { AscApiService, AscSessionStorageAuthService, AscAuthUser } from '@aseansc-admin/sea-http';
import { tap } from 'rxjs';
interface MyLoginResponse extends AscAuthUser {
fullName: string;
userCode: string;
permissionList: { functionId: string; rightIdList: string[] }[];
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private api = inject(AscApiService);
// or AscLocalStorageAuthService / AscCookieAuthService — see Auth Storage
private storage = inject(AscSessionStorageAuthService);
login(username: string, password: string) {
return this.api.login<MyLoginResponse>({
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" }
}
}File Upload — AscApiService.postFormData()
Gọi API envelope kèm file upload (multipart/form-data). Cùng unwrap body.data / throw AscApiError như post(), nhưng không dùng chung cơ chế inject userID của ascAuthInterceptor: interceptor chỉ inject userID bằng cách mutate JSON body, còn với FormData thì nó không "nhìn" được vào bên trong — nên userID sẽ luôn rỗng nếu để mặc định. postFormData() tự lấy userID thật trực tiếp qua ASC_HTTP_USER_ID_FN ngay lúc build header, không phụ thuộc interceptor. Bearer token vẫn được interceptor tự inject bình thường (không phân biệt body type).
Envelope { header, body: { authenType, data } } được đóng gói thành 1 field multipart tên request (JSON.stringify); mỗi file trong files được append riêng theo field tương ứng.
import { inject, Injectable } from '@angular/core';
import { AscApiService } from '@aseansc-admin/sea-http';
interface UploadResult { url: string; }
@Injectable({ providedIn: 'root' })
export class ProfileService {
private api = inject(AscApiService);
uploadAvatar(userId: string, file: File) {
return this.api.postFormData<UploadResult>({
authenType: 'uploadAvatar',
data: { userId },
files: [{ field: 'avatar', file }],
});
}
uploadWithAttachments(data: Record<string, unknown>, attachments: File[]) {
return this.api.postFormData<UploadResult>({
authenType: 'submitTicket',
data,
files: attachments.map((file, i) => ({ field: `attachment${i}`, file })),
});
}
// file: null → xoá avatar hiện có (theo quy ước BE: field multipart rỗng = xoá).
// Không đưa field vào `files` nếu muốn giữ nguyên file cũ.
removeAvatar(userId: string) {
return this.api.postFormData<UploadResult>({
authenType: 'updateProfile',
data: { userId },
files: [{ field: 'avatar', file: null }],
});
}
}AscPostFormDataOptions:
| Option | Type | Required | Description |
|---------------|-------------------------------------------|----------|--------------------------------------------------------------|
| authenType | string | ✅ | Tên action/operation, vd 'uploadAvatar' |
| data | TBody | ✅ | Payload nghiệp vụ — đóng gói cùng header vào field request |
| files | { field: string; file: File \| null }[] | ✅ | Danh sách file — mỗi phần tử tạo 1 field multipart riêng. file: null → gửi field rỗng (''), theo quy ước BE nghĩa là xoá file đó khỏi record |
| endpoint | string | — | Named endpoint trong AscHttpConfig.endpoints |
| skipLoading | boolean | — | Bỏ qua global loading indicator |
Plain REST — AscApiService.request()
Dùng cho endpoint BE trả thẳng chuẩn HTTP (không bọc envelope { header, body }). Đi qua cùng interceptor chain với post()/login() — Bearer token vẫn tự inject, lỗi vẫn tự xử lý qua ascErrorInterceptor + ASC_HTTP_ERROR_HANDLER, loading indicator vẫn tự đếm. Khác biệt duy nhất: không build/unwrap envelope, và lỗi luôn là HttpErrorResponse chuẩn (không có AscApiError).
import { inject, Injectable } from '@angular/core';
import { AscApiService } from '@aseansc-admin/sea-http';
interface User { id: string; name: string; }
interface CreateUserDto { name: string; email: string; }
@Injectable({ providedIn: 'root' })
export class UserService {
private api = inject(AscApiService);
getById(id: string) {
return this.api.request<User>({ path: `/users/${id}` });
}
list(status: 'active' | 'inactive') {
return this.api.request<User[]>({ path: '/users', params: { status } });
}
create(dto: CreateUserDto) {
return this.api.request<User, CreateUserDto>({ method: 'POST', path: '/users', body: dto });
}
delete(id: string) {
return this.api.request<void>({ method: 'DELETE', path: `/users/${id}` });
}
}AscRestOptions:
| Option | Type | Default | Description |
|---------------|----------------------------------------------------------|---------|------------------------------------------------------------|
| method | 'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' | 'GET' | HTTP method |
| path | string | — | Nối vào baseUrl/endpoints[endpoint], vd /users/123 |
| body | TBody | — | Request body — POST/PUT/PATCH/DELETE |
| params | Record<string, string \| number \| boolean \| null \| undefined> | — | Query params — null/undefined bị bỏ qua |
| headers | Record<string, string> | — | Header bổ sung (không ghi đè Authorization) |
| endpoint | string | — | Named endpoint trong AscHttpConfig.endpoints |
| skipLoading | boolean | false | Bỏ qua global loading indicator |
Khi nào dùng
post()/login()/postFormData()vsrequest()? Dùngpost()/login()/postFormData()cho BE theo chuẩn envelope cũ (sea-meetings,sea-auth...) —postFormData()là biến thể củapost()khi cần gửi kèm file. Dùngrequest()cho BE mới trả thẳng REST chuẩn HTTP — các flow chạy song song, không xung đột, dùng chung 1AscApiService/interceptor chain.
Auth Storage
Three ready-made services for persisting the token and user session. All 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<MyLoginResponse>(); // MyLoginResponse | null — pass your own type
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<MyLoginResponse>();
storage.clear();Cookies are set with SameSite=Strict; path=/.
AscSessionStorageAuthService — sessionStorage
Cleared automatically when the tab/browser is closed — use when the session shouldn't outlive the tab.
import { inject } from '@angular/core';
import { AscSessionStorageAuthService } from '@aseansc-admin/sea-http';
const storage = inject(AscSessionStorageAuthService);
storage.save(loginResponseData);
storage.getToken();
storage.getUser<MyLoginResponse>();
storage.clear();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 / AscSessionStorageAuthService
} 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); every other error status (400, 403, 404, network, 5xx...) goes through ASC_HTTP_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
ascLoadingInterceptor tự động đếm request đang active qua AscLoadingService. Mặc định BẬT cho mọi request.
Dùng AscLoadingBarComponent từ @aseansc-admin/ui để hiển thị thanh loading đầu trang:
// app.component.ts
import { AscLoadingService } from '@aseansc-admin/sea-http';
loading = inject(AscLoadingService);<!-- app.component.html -->
<asc-loading-bar [visible]="loading.isLoading()" />
<p-toast />
<p-confirmdialog />
<router-outlet />Tắt loading cho request cụ thể
Dùng skipLoading: true trong AscApiService.post() để bỏ qua loading indicator — hữu ích cho background polling, auto-refresh, silent call:
// Loading bật — thanh loading hiện khi gọi API này
this.api.post({ authenType: 'getData', data: {} })
// Loading tắt — call âm thầm, không ảnh hưởng UI
this.api.post({ authenType: 'silentPoll', data: {}, skipLoading: true })
// Tương tự với request() (REST thuần)
this.api.request({ path: '/status', skipLoading: true })Hoặc dùng SKIP_LOADING token trực tiếp với HttpClient:
import { HttpContext } from '@angular/common/http';
import { SKIP_LOADING } from '@aseansc-admin/sea-http';
this.http.post(url, body, {
context: new HttpContext().set(SKIP_LOADING, true),
})Error Handling
API-level errors (when body.status !== 'OK', only for post()/login()) 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 (any non-2xx status, network errors) are handled automatically by ascErrorInterceptor for both post()/login() and request() — since it works purely off HttpErrorResponse.status, not the envelope shape:
401→ clears session, redirects to/login(does not call the error handler)- everything else (
400,403,404,0network error,5xx...) → passed toASC_HTTP_ERROR_HANDLER 500retries up to 3× (1s/2s/3s delay) before falling through to the error handler
request() never throws AscApiError — its errors are always plain HttpErrorResponse.
Testing with MSW (Mock Service Worker)
Import từ secondary entry point @aseansc-admin/sea-http/testing — tách biệt hoàn toàn với production bundle, MSW không bao giờ được ship.
Install
npm install [email protected] --save-dev1. Khởi tạo MSW service worker (browser)
npx msw init public/ --saveTạo file src/mocks/browser.ts:
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);Khởi động worker trong main.ts khi dev:
// main.ts
if (isDevMode()) {
const { worker } = await import('./mocks/browser');
await worker.start({ onUnhandledRequest: 'bypass' });
}
bootstrapApplication(AppComponent, appConfig);2. Khởi tạo MSW server (Jest / Vitest)
// jest.setup.ts (hoặc vitest.setup.ts)
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';
export const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());3. Tạo handlers
// src/mocks/handlers.ts
import { createAscHandler, ascOperation } from '@aseansc-admin/sea-http/testing';
export const handlers = [
createAscHandler('https://api.example.com/api', [
// ── OK response ───────────────────────────────────────────────────────
ascOperation('getAllMeeting', () => ({
meetings: [
{ id: '1', title: 'Kickoff Q3', date: '2026-07-01' },
{ id: '2', title: 'Sprint Review', date: '2026-07-08' },
],
total: 2,
})),
// ── Đọc payload từ request ────────────────────────────────────────────
ascOperation('getMeetingById', ({ data }) => {
const req = data as { id: string };
return { id: req.id, title: `Meeting ${req.id}`, status: 'ACTIVE' };
}),
// ── Login ─────────────────────────────────────────────────────────────
// sea-http dùng body.command thay vì authenType cho login
ascOperation('getLogin', ({ data }) => {
const creds = data as { username: string };
return { token: 'mock-jwt-token', userCode: creds.username, stateType: 'LOGIN' };
}),
]),
];4. Error cases
4a. Business error — errorStatus
sea-http ném AscApiError khi body.status !== 'OK'. HTTP status vẫn là 200.
// AscApiError { status: 'USER_NOT_FOUND', data: { id: '99' } }
ascOperation(
'getUserById',
({ data }) => ({ id: (data as any).id }),
{ errorStatus: 'USER_NOT_FOUND' },
)
// AscApiError { status: 'VALIDATION_ERROR', data: { field: 'email', msg: 'Invalid email' } }
ascOperation(
'createUser',
() => ({ field: 'email', msg: 'Invalid email' }),
{ errorStatus: 'VALIDATION_ERROR' },
)
// Dùng trực tiếp ascFail nếu cần tuỳ chỉnh ngoài ascOperation
import { ascFail } from '@aseansc-admin/sea-http/testing';
return HttpResponse.json(ascFail('LOCKED', { retryAfter: 60 }));4b. HTTP 401 Unauthorized
ascErrorInterceptor bắt 401 → xoá session + redirect /login tự động.
ascOperation('getSecureData', () => null, { httpStatus: 401 })Lưu ý: Resolver KHÔNG được gọi khi
httpStatusđược đặt.
4c. HTTP 403 Forbidden
ascErrorInterceptor bắt 403 → gọi ASC_HTTP_ERROR_HANDLER.
ascOperation('deleteRecord', () => null, { httpStatus: 403 })4d. HTTP 500 Internal Server Error
ascErrorInterceptor tự retry tối đa 3 lần (delay 1 s / 2 s / 3 s), sau đó gọi ASC_HTTP_ERROR_HANDLER.
Mock này hữu ích để kiểm tra xem app có hiện đúng trạng thái loading và thông báo lỗi sau khi hết retry không.
ascOperation('getReport', () => null, { httpStatus: 500 })
// Kết hợp với delay để thấy rõ retry effect
ascOperation('getReport', () => null, { httpStatus: 500, delayMs: 300 })4e. HTTP 404 Not Found
ascErrorInterceptor gọi ASC_HTTP_ERROR_HANDLER (giống mọi status khác ngoài 401/0), sau đó vẫn ném HttpErrorResponse về subscriber.
ascOperation('getAttachment', () => null, { httpStatus: 404 })4f. Network error (status 0)
Giả lập mất kết nối mạng (connection refused, timeout). Angular nhận HttpErrorResponse { status: 0 }.ascErrorInterceptor gọi ASC_HTTP_ERROR_HANDLER với statusText: 'Network Error — Không kết nối được server'.
ascOperation('getData', () => null, { networkError: true })Lưu ý:
networkErrorvàhttpStatusđều bỏ qua resolver. Nếu đặt cả hai cùng lúc,networkErrorđược ưu tiên.
5. Kết hợp delay + error để test loading UI
// Thấy loading spinner, sau 2 giây hiện lỗi 500 → retry 3 lần → hiện toast error
ascOperation('heavyReport', () => null, { httpStatus: 500, delayMs: 2000 })
// Thấy loading, sau 800 ms hiện lỗi nghiệp vụ
ascOperation('submitForm', () => ({ msg: 'Duplicate entry' }), {
errorStatus: 'DUPLICATE',
delayMs: 800,
})6. Override trong từng test
Dùng server.use() để override handler tạm thời trong một test case — không ảnh hưởng test khác nhờ server.resetHandlers() sau mỗi test.
import { server } from '../jest.setup';
import { createAscHandler, ascOperation } from '@aseansc-admin/sea-http/testing';
it('hiện thông báo lỗi khi API trả về UNAUTHORIZED', async () => {
server.use(
createAscHandler('https://api.example.com/api', [
ascOperation('getProfile', () => null, { errorStatus: 'UNAUTHORIZED' }),
]),
);
// ... render component, kiểm tra toast / error message
});
it('redirect về /login khi server trả về 401', async () => {
server.use(
createAscHandler('https://api.example.com/api', [
ascOperation('getProfile', () => null, { httpStatus: 401 }),
]),
);
// ... kiểm tra router.navigate đã được gọi với ['/login']
});
it('hiện banner lỗi mạng khi mất kết nối', async () => {
server.use(
createAscHandler('https://api.example.com/api', [
ascOperation('getProfile', () => null, { networkError: true }),
]),
);
// ... kiểm tra error handler được gọi với status 0
});7. Nhiều endpoints
Nếu app dùng nhiều endpoints trong provideAscHttp, tạo nhiều handler riêng:
import { createAscHandler, ascOperation } from '@aseansc-admin/sea-http/testing';
export const handlers = [
// baseUrl
createAscHandler('https://api.example.com/api', [
ascOperation('getAllMeeting', () => ({ meetings: [] })),
]),
// endpoints.auth
createAscHandler('https://auth.example.com/api', [
ascOperation('getLogin', () => ({ token: 'mock-token', userCode: 'admin' })),
]),
];Hoặc dùng wildcard để khớp mọi origin (hữu ích khi base URL thay đổi theo môi trường):
createAscHandler('*/api', [...operations])8. Bảng tham chiếu
| Option | Type | Resolver gọi? | Mô tả |
|---|---|---|---|
| (mặc định) | — | ✅ | Trả OK envelope, resolver result vào body.data |
| errorStatus | string | ✅ | Trả error envelope với body.status = errorStatus → sea-http ném AscApiError |
| httpStatus | number | ❌ | Trả HTTP error response (401/403/500...) — bypass envelope |
| networkError | boolean | ❌ | Giả lập mất kết nối — Angular nhận HttpErrorResponse.status = 0 |
| delayMs | number | — | Thêm delay (ms) trước khi trả response, kết hợp được với mọi option trên |
| HTTP Status | Xử lý bởi ascErrorInterceptor |
|---|---|
| 401 | Redirect /login (không gọi error handler) |
| 500 | Retry 3× (1s/2s/3s) → gọi ASC_HTTP_ERROR_HANDLER |
| 0 (network) | Gọi ASC_HTTP_ERROR_HANDLER với 'Network Error' |
| 403, 404, 400, khác | Gọi 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 (see Auth Storage) 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,
// Plain REST
AscHttpMethod,
AscRestOptions,
// File upload
AscPostFormDataOptions,
AscFormDataFile,
// Login
LoginRequestData,
AscAuthUser,
// SSE
AscSseOptions,
AscSseEvent,
// WebSocket
AscWsOptions,
AscWsMessage,
AscWsHeader,
AscWsStatus,
} from '@aseansc-admin/sea-http';
import { AscApiError, isAscApiRequest } from '@aseansc-admin/sea-http';