npm package: @sejatordev/ws-client
Description: WebSocket client ringan dengan konsep **channel-based** (mirip Pusher), mendukung **auto reconnect**, **manual reconnect**, **heartbeat**, dan **pong timeout**.
Latest version: 1.0.0

# @sejator/ws-client

WebSocket client ringan dengan konsep **channel-based** (mirip Pusher), mendukung **auto reconnect**, **manual reconnect**, **heartbeat**, dan **pong timeout**.

Cocok untuk:

- Real-time order & notifikasi
- Dashboard admin
- Public / private channel
- Browser & bundler modern (ESM / UMD)

---

## ✨ Fitur

- 🔌 Auto connect & auto reconnect
- 🔁 Manual reconnect API
- ❤️ Heartbeat + pong timeout protection
- 📡 Channel & event system
- 🔄 Auto re-subscribe saat reconnect
- 🔐 Private channel authentication
- 🛡️ CSRF token support (cookie-based)
- 🧠 Fully typed event (TypeScript)
- 🌐 URL auto normalize (http → ws, /ws)

---

## 🔗 Implementasi (SDK untuk @sejator/ws-server)

`@sejator/ws-client` dirancang khusus sebagai SDK resmi untuk berkomunikasi dengan WebSocket Server [@sejator/ws-server](https://github.com/sejator/ws-server).

Client ini mengikuti penuh protokol, event, dan format payload yang digunakan oleh `@sejator/ws-server`, sehingga:

- ✅ Plug & Play (tanpa konfigurasi rumit)
- ✅ Auto kompatibel dengan:
  - Channel system
  - Private channel authentication
  - Heartbeat & pong
  - Reconnect strategy
- ❌ Tidak ditujukan untuk WebSocket server lain yang tidak mengikuti spesifikasi `@sejator/ws-server`

> ⚠️ PENTING  
> Pastikan backend WebSocket kamu menggunakan `@sejator/ws-server`, karena:
>
> - Event seperti `connect_success`, `auth_failed`, `pong`, dll
> - Struktur auth (`APP_KEY:signature`)
> - Endpoint default (`/ws`)  
>   Semuanya bergantung pada implementasi server tersebut.

---

## 🔄 Arsitektur Client ↔ Server

```txt
Browser / Frontend
        │
        │  @sejator/ws-client
        ▼
WebSocket Protocol (Channel-based)
        │
        │  @sejator/ws-server
        ▼
Application Backend
```

---

## ⚙️ Cara Kerja

1. Client melakukan koneksi ke endpoint WebSocket server (`/ws`)
2. Server membalas dengan:
   - `socket_id`
   - `activity_timeout`
3. Client:
   - Menjaga koneksi dengan heartbeat
   - Auto reconnect jika terputus
4. Saat subscribe `private-*` channel:
   - Client meminta auth ke backend
   - Server memverifikasi signature
5. Event dari server langsung dipetakan ke channel client

---

## 📦 Instalasi

### ✅ Opsi 1 — Clone dari GitHub (Recommended)

```bash
git clone https://github.com/sejator/ws-client.git
cd ws-client
npm install
npm run build

```

Hasil build tersedia di folder:

```txt
dist/
├─ ws-client.es.js
├─ ws-client.umd.js
├─ index.d.ts
```

### ✅ Opsi 2 — Import manual ke project (ESM)

Copy file hasil build ke project kamu:

```txt
dist/ws-client.es.js
```

Lalu import:

```ts
import { WSClient } from '@sejator/ws-client';
```

### ✅ Opsi 3 — Browser (UMD tanpa bundler)

Gunakan file UMD hasil build (ws-client.umd.js):

```html
<script src="dist/ws-client.umd.js"></script>
<script>
  const { WSClient } = window.WSClient;
</script>
```

---

## 🚀 Penggunaan

### 1️⃣ ESM (Vite / Next.js / Node modern)

```ts
import { WSClient } from '@sejator/ws-client';

const client = new WSClient('http://localhost:3000', {
  key: 'YOUR_APP_KEY',
  logger: true,
});

client.on('connect_success', (data) => {
  console.log('Connected with socket id:', data.socket_id);
});

client.on('state_change', ({ previous, current }) => {
  console.log('State:', previous, '→', current);
});

client.on('reconnecting', ({ manual }) => {
  console.log('Reconnecting...', manual ? '(manual)' : '(auto)');
});

client.on('auth_failed', (err) => {
  console.error('Auth failed:', err);
});

client.on('error', (err) => {
  console.error('WebSocket error', err);
});

const channel = client.subscribe('public-orders');

channel.bind('order.created', (payload) => {
  console.log('Order created:', payload);
});

channel.bindAll((event, data) => {
  console.log('Event All:', event, data);
});

// unsubscribe dari channel
channel.unsubscribe();

// remove event dari channel
channel.unbind('order.created');
```

---

### 2️⃣ UMD (Browser tanpa bundler)

```html
<script src="/dist/ws-client.umd.js"></script>
<script>
  const { WSClient } = window.WSClient;

  const client = new WSClient('http://localhost:3000', {
    key: 'YOUR_APP_KEY',
    logger: true,
  });

  client.on('connect_success', () => {
    console.log('Connected...');
  });

  const channel = client.subscribe('public-orders');

  channel.bind('order.created', (data) => {
    console.log('Order created:', data);
  });
</script>
```

📌 **Namespace UMD**: `window.WSClient`

---

## 📺 Public Channel

```ts
const channel = client.subscribe('public-orders');
atau;
const channel = client.subscribe('orders');

channel.bind('order.created', (data) => {
  console.log(data);
});

channel.unbind('order.created');
```

---

## 🔐 Private Channel

Channel dengan prefix private- memerlukan auth token dari backend.

```ts
const channel = client.subscribe('private-orders');

channel.bind('order.updated', (data) => {
  console.log(data);
});
```

---

## 🔌 Client Options

```ts
interface WSClientOptions {
  key: string; // required

  logger?: boolean; // default false
  reconnect?: boolean; // default true
  maxReconnectDelay?: number; // default 30000 (ms)

  // 🔐 Auth options
  authEndpoint?: string;
  authHeaders?: () => Record<string, string>;
  auth?: (params: {
    socketId: string;
    channel: string;
    headers?: Readonly<Record<string, string>>;
    query?: Record<string, string>;
  }) => Promise<{ auth: string }>;
}
```

## 🔐 Authentication (Auth)

Ada 2 cara untuk melakukan auth private channel.

#### ⚠️ PENTING — Response Backend

Backend WAJIB mengembalikan response JSON dengan minimal field berikut:

```json
{
  "data": {
    "auth": "APP_KEY:signature_hash"
  }
}
```

Format response yang direkomendasikan:

```json
{
  "success": true,
  "code": 200,
  "data": {
    "auth": "APP_KEY:signature_hash"
  }
}
```

### 📌 Catatan:

- Field data.auth WAJIB ada dan bertipe string
- Field lain (success, code, dll) opsional
- Jika data.auth tidak ada / null:
  - ❌ Subscribe private channel akan gagal
  - 🔔 Client memicu event `auth_failed`

---

### ✅ Opsi 1 — authEndpoint (paling mudah)

Client akan otomatis melakukan POST ke endpoint backend.

```ts
const client = new WSClient('http://localhost:3000', {
  key: 'YOUR_APP_KEY',
  authEndpoint: '/ws/auth',
});
```

Yang dikirim ke backend:

```json
{
  "socket_id": "...",
  "channel": "private-orders"
}
```

- ✔ Cookie otomatis dikirim
- ✔ CSRF token otomatis ditambahkan (X-XSRF-TOKEN)

---

### ✅ Opsi 2 — auth function (fleksibel)

Gunakan jika kamu ingin custom fetch / axios / logic sendiri.

```ts
const client = new WSClient('http://localhost:3000', {
  key: 'YOUR_APP_KEY',
  auth: async ({ socketId, channel, headers }) => {
    const res = await fetch('/ws/auth', {
      method: 'POST',
      credentials: 'include',
      headers,
      body: JSON.stringify({
        socket_id: socketId,
        channel,
      }),
    });

    const json = await res.json();
    return { auth: json.data.auth };
  },
});
```

---

## 🛡️ CSRF Protection

Client secara otomatis:

- Mengambil token dari cookie XSRF-TOKEN
- Mengirimkannya sebagai header X-XSRF-TOKEN

Tidak perlu konfigurasi tambahan di client.

📌 Cocok untuk:

- Laravel
- Django
- Express (csurf)

---

## 📡 Event Client

| Event             | Payload                           | Deskripsi                                     |
| ----------------- | --------------------------------- | --------------------------------------------- |
| `connect_success` | `{ socket_id, activity_timeout }` | Koneksi berhasil                              |
| `disconnected`    | `void`                            | Socket terputus                               |
| `reconnecting`    | `{ manual: boolean }`             | Reconnect dimulai                             |
| `auth_failed`     | `string`                          | App key tidak valid/Tidak punya akses channel |
| `state_change`    | `{ previous, current }`           | Perubahan state client                        |
| `error`           | `unknown`                         | Error WebSocket                               |

---

## 🔁 Reconnect Strategy

- Exponential backoff
- Auto retry sampai `maxReconnectDelay`
- Manual reconnect tersedia

```ts
client.reconnect();
```

---

## ❤️ Heartbeat & Pong Timeout

- Client mengirim ping
- Server membalas pong
- Timeout → force reconnect

---

## 🌐 URL Normalization

`@sejator/ws-client` menerima URL HTTP maupun WebSocket, lalu menormalisasikannya otomatis agar sesuai dengan endpoint WebSocket server.

Apa yang dilakukan client?

Client akan:

#### 1. Mengubah protokol:

- `http://` → `ws://`

- `https://` → `wss://`

#### 2. Menambahkan path WebSocket default /ws

#### 3. Menghapus trailing slash (/) jika ada

Dengan begitu, client selalu terhubung ke endpoint WebSocket yang benar tanpa konfigurasi tambahan.

---

Contoh

Input URL dari user:

```ts
new WSClient('http://localhost:3000', { key: 'APP_KEY' });
```

Akan otomatis dinormalisasi menjadi:

```txt
ws://localhost:3000/ws
```

Contoh lain:
| Input | URL akhir |
| ------------------------- | -------------------------- |
| `https://api.example.com` | `wss://api.example.com/ws` |
| `ws://localhost:3000` | `ws://localhost:3000/ws` |
| `localhost:3000` | `ws://localhost:3000/ws` |

---

## 📦 Build

```bash
npm run build
```

Output:

```txt
dist/
├─ ws-client.es.js
├─ ws-client.umd.js
├─ *.map
├─ index.d.ts
```

---

## Kontak & Dukungan

Maintainer: [@sejator](https://github.com/sejator)\
Email: [sejatordev@gmail.com](sejatordev@gmail.com)\
Donasi: [https://saweria.co/sejator](https://saweria.co/sejator)