# Plan: Extension APIs & Messaging

## Bối cảnh

### Hiện trạng `src/app/browser/api.js`

Dự án wrap `chrome.*` thủ công thay vì dùng `browser` unified object của WXT:

```js
// Hiện tại — hardcode chrome.*
export const browserLocalStorage = chrome.storage ? chrome.storage.local : null;
export const browserWebrequest = chrome.webRequest ? chrome.webRequest : null;
export const browserOnMessage = chrome.runtime ? chrome.runtime.onMessage : null;
```

**Vấn đề:**
- Mỗi API check null riêng (`chrome.xxx ? ... : null`) — boilerplate
- Firefox dùng `browser.*` — code này sẽ crash trên Firefox nếu `chrome` không tồn tại
- `isDevMode` mới inject `webextension-polyfill` — production Firefox không có polyfill

### Hiện trạng Messaging

Dự án có **2 lớp messaging** chạy song song:

**Lớp 1 — `webext-bridge`** (ISOLATED world ↔ Background ↔ Popup):

```
Content Script (ISOLATED) ──sendMessage──► Background
Background ──sendMessage──► Content Script (ISOLATED)
```

**Lớp 2 — `window.postMessage` custom** (MAIN world ↔ ISOLATED world):

```
shopee-fetch.content.js (MAIN)
  │ window.postMessage (PostMessageFetchUrl / PostMessageFetchListener)
  ▼
content script (ISOLATED)
  │ webext-bridge sendMessage
  ▼
Background
```

Lý do cần lớp 2: `webext-bridge` không khả dụng ở `world: 'MAIN'` vì không có `chrome.runtime`. MAIN world phải relay qua ISOLATED world.

---

## Phần 1 — Extension APIs: Migrate sang `browser` của WXT

### Vấn đề cụ thể

`src/app/browser/api.js` import trực tiếp `chrome.*` ở top-level module — crash khi WXT import file này trong Node.js context (build/prepare):

```js
// Crash khi import ở top level — chrome không tồn tại trong Node
export const browserLocalStorage = chrome.storage ? chrome.storage.local : null;
```

Hiện tại không crash vì file này chỉ được import từ bên trong `main()` của entrypoints (gián tiếp qua plugin/store). Nhưng nếu vô tình import ở top-level entrypoint sẽ lỗi.

### Cách fix

Thay `chrome.*` bằng `browser` từ WXT:

```js
// src/app/browser/api.js — sau khi migrate
import { browser } from 'wxt/browser';

export const browserLocalStorage = browser.storage.local;
export const browserWebrequest = browser.webRequest;
export const browserOnMessage = browser.runtime.onMessage;
export const browserSendMessage = browser.runtime.sendMessage;
export const browserAlarms = browser.alarms;
// ...
```

`browser` của WXT tự merge `chrome` (Chromium) và `browser` (Firefox) — không cần check null thủ công, không cần polyfill riêng.

**Xóa đoạn polyfill thủ công:**

```js
// Xóa đoạn này — không cần nữa
if (isDevMode) {
  import('webextension-polyfill').then((polyfill) => {
    globalThis.browser = polyfill.default || polyfill;
  });
}
```

### Phạm vi thay đổi

| File | Thay đổi |
|---|---|
| `src/app/browser/api.js` | Thay `chrome.*` → `browser.*` từ `wxt/browser` |
| Không cần sửa | Các file import từ `browser/api.js` giữ nguyên |

### Lưu ý — API không tồn tại ở mọi context

Một số API chỉ có ở background hoặc một browser nhất định. Dùng optional chaining khi không chắc:

```js
// webRequest chỉ có ở background — content script không có
export const browserWebrequest = browser.webRequest ?? null;

// Feature detection tường minh
if (browser.webRequest) {
  browser.webRequest.onBeforeSendHeaders.addListener(...);
}
```

---

## Phần 2 — Messaging: Cải thiện pattern hiện tại

### Vấn đề 1 — Event names là plain string, dễ typo

```js
// Hiện tại — string literal, không type-safe
sendMessage('EVENT_GET_USER_INFO', payload);
onMessage('EVENT_GET_USER_INFO', handler);
```

Nếu typo tên event, không có lỗi compile — silent bug.

### Fix — Dùng constant từ `event.js` nhất quán

`src/app/bridge/config/event.js` đã có constants, nhưng một số nơi vẫn dùng string literal. Cần enforce dùng constant:

```js
import { EVENT_GET_USER_INFO } from '@/bridge/config/event';

sendMessage(EVENT_GET_USER_INFO, payload);  // dùng constant
onMessage(EVENT_GET_USER_INFO, handler);
```

Thêm ESLint rule để bắt string literal trong `sendMessage`/`onMessage` nếu muốn enforce.

---

### Vấn đề 2 — `PostMessageApi` (MAIN ↔ ISOLATED) có timeout cứng, không retry

```js
// src/app/bridge/content/postMessage.js
export const PostMessageFetchUrl = new PostMessageApi('FETCH_URL', 5000);
export const PostMessageFetchListener = new PostMessageApi('FETCH_LISTENER', 1000);
```

Nếu ISOLATED world chưa sẵn sàng khi MAIN world gửi message → timeout 1000ms → reject. Không có retry.

**Fix — tăng timeout cho `PostMessageFetchListener` hoặc thêm retry:**

```js
export const PostMessageFetchListener = new PostMessageApi('FETCH_LISTENER', 3000);
```

Hoặc nếu muốn robust hơn, thêm queue vào `PostMessageApi.sendMessage()` — không reject khi timeout mà queue lại và retry sau.

---

### Vấn đề 3 — `EVENT_GET_LOCAL_SETTING` bridge không cần thiết

Content script hiện gọi background để lấy setting:

```js
// content script → background → trả về setting value
const value = await sendMessage(EVENT_GET_LOCAL_SETTING, { key: 'shopeeShopId' });
```

Background handler chỉ làm:
```js
const onBridgeGetLocalSetting = async ({ data }) => {
  return await allSetting[data.key].getSync();
};
```

`getSync()` đọc từ `chrome.storage.local` — đây là operation content script có thể tự làm trực tiếp, không cần relay qua background.

**Fix — content script đọc storage trực tiếp:**

```js
// Trước — qua bridge
const shopId = await bridgeGetSetting('shopeeShopId');

// Sau — đọc trực tiếp (nếu đã dùng @wxt-dev/storage)
import { storageShopeeShopId } from '@/plugins/storage/items';
const shopId = await storageShopeeShopId.getValue();
```

Loại bỏ được 1 round-trip message, giảm latency.

---

### Vấn đề 4 — Background → Content Script messaging thiếu error handling

```js
// src/app/bridge/background/content.js
export const bridgeTabFetchShopeeUrl = async (tabId, payload = {}) => {
  return sendMessageTabId(tabId, EVENT_TAB_FETCH_URL, payload);
};
```

Nếu tab đã đóng hoặc content script chưa inject, `sendMessage` throw lỗi "Receiving end does not exist". Caller không wrap try/catch.

**Fix — wrap trong bridge function:**

```js
export const bridgeTabFetchShopeeUrl = async (tabId, payload = {}) => {
  try {
    return await sendMessageTabId(tabId, EVENT_TAB_FETCH_URL, payload);
  } catch (error) {
    if (error?.message?.includes('Receiving end does not exist')) {
      return null; // tab đã đóng hoặc content script chưa ready
    }
    throw error;
  }
};
```

Có thể xem thêm: `docs/dev/debug/receiving-end-does-not-exist.md`

---

## Ưu tiên

| Item | Impact | Effort |
|---|---|---|
| Fix `browser/api.js` dùng `browser` từ WXT | Firefox compat, cleaner | Thấp |
| Fix error handling cho background→content bridge | Giảm noise lỗi | Thấp |
| Bỏ `EVENT_GET_LOCAL_SETTING` bridge (dùng storage trực tiếp) | Giảm latency | Trung bình (cần kết hợp wxt-storage plan) |
| Tăng timeout `PostMessageFetchListener` | Giảm race condition | Rất thấp |

---

## Không thay đổi

- Kiến trúc 2 lớp messaging (webext-bridge + postMessage) — giữ nguyên, đây là workaround cần thiết cho MAIN world
- `webext-bridge` package — WXT cũng khuyến nghị package này, không cần migrate
- Tên event constants trong `event.js` — chỉ enforce dùng constant, không đổi tên
