npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-http

Peer 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() vs request()? Dùng post()/login()/postFormData() cho BE theo chuẩn envelope cũ (sea-meetings, sea-auth...) — postFormData() là biến thể của post() khi cần gửi kèm file. Dùng request() 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 1 AscApiService/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 keys

AscCookieAuthService — 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, 0 network error, 5xx...) → passed to ASC_HTTP_ERROR_HANDLER
  • 500 retries 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-dev

1. Khởi tạo MSW service worker (browser)

npx msw init public/ --save

Tạ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 ý: networkErrorhttpStatus đề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\n
import { 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\n

Use 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ọi conn.close() để cleanup subscription, nhưng vẫn nên gọi conn.close() trong ngOnDestroy để đó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';