# WXT API Migration Overview

Tổng hợp các API của WXT nên dùng thay cho cách tiếp cận hiện tại. Mục đích: **đồng bộ hóa**, giảm boilerplate, tận dụng lifecycle management tự động.

> Chi tiết từng phần xem: `wxt-storage.md`, `wxt-messaging.md`, `wxt-scripting.md`

---

## 1. `browser` từ `wxt/browser` — thay `chrome.*` thủ công

### Hiện tại

```js
// src/app/browser/api.js
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;
```

Mỗi API phải check null thủ công, không cross-browser.

### Nên dùng

```js
import { browser } from 'wxt/browser';

export const browserLocalStorage = browser.storage.local;
export const browserWebrequest = browser.webRequest;
export const browserOnMessage = browser.runtime.onMessage;
```

- Tự merge `chrome` (Chromium) và `browser` (Firefox)
- Không cần null check
- Xóa được đoạn import `webextension-polyfill` thủ công

### Files ảnh hưởng

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

### Effort: Thấp | Impact: Cao (Firefox compat)

### Đã triển khai (2026-04-06)

| File | Thay đổi |
|---|---|
| `src/app/browser/api.js` | Đã dùng `browser` từ `wxt/browser` (trước đó) |
| `src/background/index.js` | `chrome.runtime.onMessage/onConnect` → `browser.runtime` |
| `src/background/bridge/global.js` | `chrome.runtime.getURL/onConnect` → `browser.runtime` |
| `src/app/bridge/options/background.js` | `chrome.tabs.query` → `browser.tabs.query` |
| `src/app/lib/pdf/worker.js` | `chrome.runtime.getURL` → `browser.runtime.getURL` |
| `src/app/vue/router/tasks.js` | `chrome.runtime.connect/sendMessage` → `browser.runtime` |
| `src/app/vue/store/modules/router.js` | `chrome.tabs.query` → `browser.tabs.query` |
| `src/entrypoints/all-ping.content.js` | `chrome.runtime.connect/sendMessage` → `browser.runtime` |

**Đã migrate (2026-04-06)** — refactored callback → promise pattern:
- `src/background/socket/worker.js` — `chrome.runtime.sendMessage` callback → `browser.runtime.sendMessage().catch()`
- `src/app/browser/tab.js` — `chrome.runtime.lastError` callback → `try/catch` async/await
- `src/app/config/get.js` — `chrome.extension.getBackgroundPage` → `browser.runtime.id` + protocol check
- `src/app/browser/api.js` — `chrome.extension.getViews` → `browser.extension?.getViews`

---

## 2. `storage` từ `wxt/storage` — thay `chrome.storage` trực tiếp

### Đã triển khai

File `src/app/plugins/storage/items.js` đã dùng `@wxt-dev/storage` với `storage.defineItem()`.
File `src/background/service/storage.js` đã dùng `.watch()`.

### Cần mở rộng

| Việc cần làm | Mô tả |
|---|---|
| ~~Thay `bridgeGetSetting` bằng `storageXxx.getValue()`~~ | ~~Content script tự đọc storage~~ — **Đã hoàn tất**: `bridgeGetSetting` không còn caller nào trong content scripts (2026-04-06) |
| Thêm `version` + `migrations` cho items hay thay đổi | `storageShopeeConfigProduct`, `storagePlatformShopConfig` |
| Dùng `storageXxx.getValue()` thống nhất trong background | Thay `allSetting[key].getSync()` pattern cũ |

### Pattern mẫu

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

// Content script — sau (đọc trực tiếp)
import { storageShopeeShopId } from '@/plugins/storage/items';
const shopId = await storageShopeeShopId.getValue();
```

### Effort: Trung bình | Impact: Cao (giảm latency, bớt bridge dependency)

---

## 3. `ContentScriptContext` (`ctx`) — thay `window.setInterval/setTimeout/addEventListener`

### Hiện tại — pattern không nhất quán

Entrypoints đã pass `ctx` vào `main(ctx)`, nhưng bên trong logic code vẫn dùng native timers:

```js
// src/content/shopee/client/loader.js:73
setInterval(() => btnImageDownloader(), 500);  // KHÔNG cleanup

// src/content/shopee/client/product/search.js:866
setInterval(checkUrlChange, 1000);  // KHÔNG cleanup
```

### Nên dùng

```js
// Tự cleanup khi content script bị invalidated (extension update, navigation)
ctx.setInterval(() => btnImageDownloader(), 500);
ctx.setTimeout(doSomething, 3000);
ctx.addEventListener(window, 'scroll', handler);
```

WXT tự remove tất cả khi context invalidated — **không cần `clearInterval` thủ công**.

### Thêm cleanup cho non-timer resources

```js
ctx.onInvalidated(() => {
  socket.close();
  observer.disconnect();
  pouchSync.cancel();
});
```

### Files cần sửa

| File | Vấn đề | Fix | Status |
|---|---|---|---|
| `src/content/shopee/client/loader.js:73-75` | `setInterval` không cleanup | `ctx.setInterval` | DONE (đã dùng ctx) |
| `src/content/shopee/client/product/search.js:848,866` | `setInterval/setTimeout` trong class | `ctx.setInterval/setTimeout` | DONE (2026-04-06) |
| `src/content/all/ping.js:8` | `setInterval` tạo port mỗi 5s | Dead code — `all-ping.content.js` thay thế | Skipped |
| `src/entrypoints/all-ping.content.js:12` | `setInterval` không cleanup | `ctx.setInterval` | DONE (2026-04-06) |
| `src/content/lazada/loader.js:34` | `setTimeout` không cleanup | `ctx.setTimeout` + export `main(ctx)` | DONE (2026-04-06) |
| `src/content/tiki/loader.js:32` | `setTimeout` không cleanup | `ctx.setTimeout` + export `main(ctx)` | DONE (2026-04-06) |
| `src/content/shopee/client/product/detail.js` | `addEventListener` trên DOM | Element-level listeners — safe (removed with element) | N/A |
| `src/content/shopee/client/product/search.js` | `MutationObserver` không cleanup khi ctx invalidated | `ctx.onInvalidated(() => observer.disconnect())` | DONE (2026-04-06) |
| `src/content/service/util.js` | `window.addEventListener('wxt:locationchange')` | `ctx.addEventListener(window, ...)` — refactored `onLocationChange(ctx, cb)` | DONE (2026-04-06) |
| `src/content/shopee/client/loader.js` | `document.addEventListener('readystatechange')` | `ctx.addEventListener(document, ...)` | DONE (2026-04-06) |
| `src/content/shopee/seller/loader.js` | `document.addEventListener('readystatechange')` | `ctx.addEventListener(document, ...)` | DONE (2026-04-06) |
| `src/content/shopee/seller/marketing/index.js` | `document.addEventListener('readystatechange')` | `ctx.addEventListener(document, ...)` | DONE (2026-04-06) |
| `src/content/shopee/seller/product/index.js` | `document.addEventListener('readystatechange')` | `ctx.addEventListener(document, ...)` | DONE (2026-04-06) |
| `src/content/shopee/seller/print.js` | `window.addEventListener('load')` + no ctx | Refactored to `export main(ctx)` + `ctx.addEventListener` | DONE (2026-04-06) |

### Effort: Thấp | Impact: Rất cao (fix memory leaks)

---

## 4. `createShadowRootUi` / `createIntegratedUi` — thay DOM injection thủ công

### Hiện tại

```js
// src/content/shopee/client/product/detail.js:239
elementAdd.innerHTML = this.loadingText();

// src/content/service/vue.js
const vueAppContainer = document.createElement('div');
vueAppContainer.id = vueAppId;
document.body.appendChild(vueAppContainer);
new Vue({ render: h => h(App) }).$mount(`#${vueAppId}`);
```

UI inject trực tiếp vào page DOM → CSS conflict với page, page scripts có thể interfere.

### Nên dùng

**`createShadowRootUi`** — cho UI cần style isolation:

```js
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';

const ui = await createShadowRootUi(ctx, {
  name: 'shopee-spy-panel',
  position: 'inline',
  anchor: '#product-detail',
  onMount(container) {
    const app = new Vue({ render: h => h(SpyPanel) });
    app.$mount(container);
    return app;
  },
  onRemove(app) {
    app.$destroy();
  },
});
ui.mount();
```

**`createIntegratedUi`** — cho UI nhẹ không cần shadow DOM:

```js
import { createIntegratedUi } from 'wxt/utils/content-script-ui/integrated';

const ui = createIntegratedUi(ctx, {
  position: 'inline',
  anchor: '#target-element',
  onMount(container) {
    container.innerHTML = '<button class="atsp-btn">Clone</button>';
  },
});
ui.mount();
```

Cả hai tự cleanup khi `ctx` invalidated.

### Ưu tiên migrate

| UI Component | Approach | Priority |
|---|---|---|
| Vue app inject (seller panel, spy panel) | `createShadowRootUi` | Cao — CSS conflicts |
| Simple buttons (clone btn, download btn) | `createIntegratedUi` | Thấp — ít conflict |
| Style inject (`appendStyle`) | ShadowRoot tự handle | Gộp với UI migrate |

### Effort: Cao | Impact: Cao (CSS isolation, auto-cleanup)

---

## 5. `browser.scripting.executeScript` — thay bridge lấy page data

### Hiện tại

Background muốn data từ page → gửi bridge message → content script đọc DOM → trả về.

### Nên dùng (cho one-shot reads)

```js
// Background — đọc trực tiếp, không cần content script
const results = await browser.scripting.executeScript({
  target: { tabId },
  func: () => document.title,
});
const title = results[0].result;
```

### Use cases phù hợp

| Use case | Pattern |
|---|---|
| Lấy data từ page một lần | `executeScript({ func })` |
| Check content script đã load | `executeScript({ func: () => !!window.__ATSP__ })` |
| Inject script vào MAIN world | `executeScript({ files: [...], world: 'MAIN' })` |

### Prerequisite

Thêm `'scripting'` vào permissions trong `wxt.config.ts` (đã có sẵn).

### Effort: Thấp | Impact: Trung bình

---

## 6. `defineUnlistedScript` — cho scripts inject theo điều kiện

### Hiện tại

Một số scripts được khai báo trong `web_accessible_resources` và inject qua `<script src>`.

### Nên dùng

```js
// entrypoints/my-injector.js
export default defineUnlistedScript({
  main() {
    // chạy trong page context
    return window.someData;
  },
});
```

Kết hợp với `browser.scripting.executeScript({ files: ['my-injector.js'] })` từ background.

### Effort: Thấp | Impact: Thấp (ít use case)

---

## Tổng hợp ưu tiên

| # | API WXT | Thay thế cái gì | Effort | Impact | Phase | Status |
|---|---|---|---|---|---|---|
| 1 | `ctx.setInterval/setTimeout/addEventListener` | Native timers không cleanup | Thấp | **Rất cao** | 1 | **DONE** |
| 2 | `ctx.onInvalidated()` | Không cleanup socket/observer/sync | Thấp | **Rất cao** | 1 | **DONE** |
| 3 | `browser` từ `wxt/browser` | `chrome.*` thủ công + null check | Thấp | Cao | 1 | **DONE** |
| 4 | `storage.defineItem().getValue()` trực tiếp | `bridgeGetSetting` qua background | Trung bình | Cao | 2 | **DONE** (không còn caller) |
| 5 | `storage.defineItem()` migrations | Không có versioning | Thấp | Cao | 2 | TODO |
| 6 | `createShadowRootUi` | `innerHTML` + `createElement` injection | Cao | Cao | 3 | TODO |
| 7 | `browser.scripting.executeScript` | Bridge round-trip lấy page data | Thấp | Trung bình | 3 | TODO |
| 8 | `createIntegratedUi` | Simple DOM buttons | Thấp | Thấp | 4 | TODO |
| 9 | `defineUnlistedScript` | `web_accessible_resources` scripts | Thấp | Thấp | 4 | TODO |

---

## Không migrate

| Cái gì | Lý do giữ nguyên |
|---|---|
| `webext-bridge` messaging | WXT docs cũng recommend, đã stable, có MAIN↔ISOLATED relay |
| `PostMessageApi` (MAIN↔ISOLATED) | Workaround cần thiết, WXT không có built-in cho MAIN world messaging |
| `apiLocalSetting` (localStorage sync read) | WXT storage là async-only, không thay được sync reads |
| KV Store (`src/app/plugins/kv/`) | Server-side remote storage, khác scope |
| PouchDB/CouchDB sync | Database layer, không liên quan WXT |
| Vuex/Pinia persistence | State management layer riêng |
