# @i.un/libs

一个实用的 TypeScript 函数库，提供跨平台的工具函数和实用程序。

[![npm version](https://img.shields.io/npm/v/@i.un/libs.svg)](https://www.npmjs.com/package/@i.un/libs)
[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)

## 📦 安装

```bash
npm install @i.un/libs
# 或
yarn add @i.un/libs
# 或
pnpm add @i.un/libs
```

## 🚀 快速开始

```typescript
import { debounce, singleFlight, createElement, cloneDeep } from '@i.un/libs';

// 防抖函数
const debouncedFn = debounce(() => {
  console.log('防抖执行');
}, 300);

// 单飞：并发调用只请求一次，所有调用方共享结果
const fetchUser = singleFlight((id: string) => fetch(`/api/users/${id}`).then(r => r.json()), {
  key: (id) => id,
});

// 创建DOM元素
const button = createElement({
  tag: 'button',
  children: '点击我',
  attrs: { id: 'myButton' },
  styles: { color: 'red' }
});

// 深度克隆
const cloned = cloneDeep({ name: 'test', items: [1, 2, 3] });
```

## 📚 功能模块

### 🔧 通用工具 (Common)

#### 防抖和延迟
- `debounce(func, wait)` - 防抖函数
- `delayExecution(callback, delay, ...args)` - 延迟执行函数，返回 Promise

#### 并发控制
- `singleFlight(func, options?)` - 单飞：并发调用共享同一次执行
- `singleExecutionWrapper(func, minTime?)` - 进行中则丢弃，立即返回 `RealUndefined`
- `coalesce(func, options?)` - 合并尾部：执行期间的触发合并成一次补跑
- `Mutex` - 互斥锁，任务串行执行
- `KeyedMutex` - 按 key 分组的互斥锁
- `serialize(func, options?)` - 把函数包成自动排队的版本

#### 对象操作
- `cloneDeep(obj)` - 深度克隆对象
- `isEqual(a, b)` - 深度相等比较
- `createDefaultObject(defaultValue)` - 创建带默认值的代理对象
- `isCloneable(obj)` - 检查对象是否可克隆
- `isPrimitive(value)` - 检查值是否为原始类型

#### 缓存
- `cacheWrapper(func, setting)` - 为异步函数添加缓存功能
- `generateStableUniqueKey(args)` - 生成稳定的唯一键

#### 存储
- `MemoryStore` - 内存存储，与 `WebStore` / `ChromeStore` 同一套接口

#### 其他
- `getGlobal()` - 获取当前环境的全局对象
- `RealUndefined` - 表示「真正的未定义」的 Symbol 哨兵值

### 🌐 浏览器工具 (Browser)

#### DOM 操作
- `createElement(options)` - 创建DOM元素
- `createSvg(params)` - 创建SVG元素
- `createSvgFromUrl(url)` - 从URL加载SVG

#### 滚动控制
- `scrollToBottom(dom)` - 滚动到底部
- `scrollToBottomIfNeeded(dom)` - 智能滚动到底部

#### 拖拽
- `Draggable` - 让元素可拖拽

#### 存储
- `WebStore` - `localStorage` / `sessionStorage` 封装

### 🔌 Chrome 扩展工具 (Chrome)

#### 存储
- `ChromeStore` - `chrome.storage.local` / `chrome.storage.sync` 封装

#### 消息通信
- `createMessageBus(config?)` - 类型安全的消息总线，统一 background / content script / tab 之间的通信
- `isErrorResponse(res)` - 判断消息响应是否为错误

#### Cookie 与扩展信息
- `getCookie(url, name, onlyValue?)` - 读取单个 cookie
- `getCookies(url, stringified?)` - 读取指定 URL 下的全部 cookie
- `getExtensionInfo()` - 获取扩展的 locale、ID、版本

#### 身份验证
- `launchWebAuthFlow(params)` - 发起 OAuth 授权流程
- `decodeJWT(token)` - 解析 JWT 的 payload

## 📖 详细文档

### 并发控制

四种原语按「并发调用发生时怎么办」区分，选错会引入很隐蔽的 bug：

| 原语 | 并发时 | 后来者拿得到结果 | 典型场景 |
| --- | --- | --- | --- |
| `singleFlight` | 复用进行中的那次 | 是，同一个 Promise | 同一份数据被多处同时请求 |
| `singleExecutionWrapper` | 丢弃，立即返回 `RealUndefined` | 否 | 重复执行无意义，且要立刻脱身 |
| `coalesce` | 合并成一次补跑 | 否（`Promise<void>`） | 触发携带新状态，丢了就丢了 |
| `Mutex` / `serialize` | 排队串行 | 是 | 读-改-写共享资源 |

四者都只管「同一时刻」的并发，**不做结果缓存**。跨次调用要不要重复执行是缓存层的职责，请配合 `cacheWrapper`。

#### singleFlight —— 共享同一次执行

```typescript
import { singleFlight } from '@i.un/libs';

const fetchProfile = singleFlight(
  (id: string) => fetch(`/api/profile/${id}`).then(r => r.json()),
  { key: (id) => id },
);

// 同一个 id 并发调用只发一个请求，两个调用方拿到同一份结果
const [a, b] = await Promise.all([fetchProfile('x'), fetchProfile('x')]);

fetchProfile.isRunning('x'); // 对应槽位是否有调用在进行中
fetchProfile.size;           // 进行中的槽位数量
```

注意：一次失败会传给所有调用方；首次调用卡住时，后来者会跟着挂。高频事件驱动的场景下，需要「立刻脱身」的请用 `singleExecutionWrapper`。

#### singleExecutionWrapper —— 进行中则丢弃

```typescript
import { singleExecutionWrapper, RealUndefined } from '@i.un/libs';

const submit = singleExecutionWrapper(async (form: FormData) => {
  await fetch('/api/submit', { method: 'POST', body: form });
}, 800); // minTime：即使任务提前完成，也至少锁定 800ms，防连点

const result = await submit(form);
if (result === RealUndefined) {
  // 上一次还没结束，本次被丢弃
}
```

#### coalesce —— 合并尾部

适用于「触发意味着状态变了，而进行中那轮读到的是旧状态」。丢弃会永久丢掉新状态，共享结果同样丢，只有补跑一次才能收敛到最新状态。

```typescript
import { coalesce } from '@i.un/libs';

const syncState = coalesce(async () => {
  const latest = await readState();
  await push(latest);
}, {
  onError: (err) => console.error(err), // 返回的 Promise 永不 reject，失败走这里
});

// 首次立即执行；执行期间的多次触发合并成结束后的一次补跑
store.subscribe(() => syncState());
```

和 `debounce` 正好相反：`debounce` 延迟首次响应，`coalesce` 首次立即执行、把合并放在尾部。两者可以叠加。

#### Mutex —— 互斥锁

读-改-写共享资源时，读和写各是一次异步调用，中间可以被别的调用插入，后写的会覆盖先写的。把整段放进同一把锁里就不会交错：

```typescript
import { Mutex } from '@i.un/libs';

const storageLock = new Mutex();

const take = (id: string) => storageLock.run(async () => {
  const map = await readMap();
  const item = map[id];
  delete map[id];
  await writeMap(map);
  return item;
});

const drop = (id: string) => storageLock.run(async () => {
  const map = await readMap();
  delete map[id];
  await writeMap(map);
});

// take 与 drop 并发时也不会交错
```

**多个函数操作同一份资源时必须共用一个 `Mutex` 实例**，各自包一层 `serialize` 是挡不住交错的。

只需要串行化单个函数时用 `serialize`；key 是动态无界的（用户 ID、tab ID 等）用 `KeyedMutex`，它会在队列排空后自动回收 key。

```typescript
import { serialize, KeyedMutex } from '@i.un/libs';

const writeProfile = serialize(async (id: string, patch: object) => { /* ... */ }, {
  key: (id) => id, // 相同 id 串行，不同 id 并行
});

const locks = new KeyedMutex();
await locks.run(userId, async () => { /* ... */ });
```

> ⚠️ 被包装的函数不能通过包装后的入口调用自己：`singleFlight` 会拿到自己那个尚未完成的 Promise 永远等下去，`Mutex` 会排在自己后面死锁。需要递归时把真正的逻辑抽成内部函数，只在最外层入口做包装。

### 防抖函数

```typescript
import { debounce } from '@i.un/libs';

const debouncedSearch = debounce((query: string) => {
  console.log('搜索:', query);
}, 300);

// 快速连续调用只会执行最后一次
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc'); // 只有这次会执行
```

### 存储

`MemoryStore` / `WebStore` / `ChromeStore` 是同一套接口的三种后端，都是按 `storageKey` 单例、带类型的键值存储：

```typescript
import { ChromeStore, WebStore, MemoryStore } from '@i.un/libs';

type UserData = { email: string; loginAt: number };

// Chrome 扩展：chrome.storage.local / sync，接口全部异步
const userStore = ChromeStore.local<UserData>('user_data');
await userStore.set('email', 'a@b.c');
const email = await userStore.get('email');

// 网页：localStorage / sessionStorage，接口同步
const prefs = WebStore.local<{ theme: 'light' | 'dark' }>('prefs');
prefs.set('theme', 'dark');

// 内存：不落盘，接口同步
const cache = MemoryStore.create<{ token: string }>('cache');

// 三者都支持
userStore.onChanged((changes) => { /* ... */ });
await userStore.setMultiple({ email: 'x@y.z', loginAt: Date.now() });
await userStore.remove('email');
await userStore.clear();
```

### 消息总线

在 background、content script、popup 之间做类型安全的通信。先定义协议表，`send` / `on` 的 payload 和 response 类型会自动推导：

```typescript
import { createMessageBus } from '@i.un/libs';

type AppMessages = {
  'user/get': { payload: void; response: { id: string; name: string } };
  'tab/highlight': { payload: { selector: string }; response: boolean };
};

const bus = createMessageBus<AppMessages>();

// background 里注册处理器（返回取消函数）
const off = bus.on('user/get', async (_payload, sender) => {
  return { id: '1', name: 'nova' };
});

// content script 里发送到 background
const user = await bus.send('user/get');

// 发送到指定 tab
const ok = await bus.sendToTab(tabId, 'tab/highlight', { selector: '.card' });

// 限定到某个 frame（如只问顶层 frame），字段与 chrome.tabs.sendMessage 的 options 一致
const alive = await bus.sendToTab({ tabId, frameId: 0 }, 'user/get');
```

### Cookie 读取

只能在扩展后台（service worker / background）或有 `cookies` 权限的扩展页面里调用：

```typescript
import { getCookie, getCookies } from '@i.un/libs';

const token = await getCookie('https://example.com', 'session', true); // 只要值
const cookie = await getCookie('https://example.com', 'session');      // 完整 Cookie 对象

const header = await getCookies('https://example.com', true); // "a=1; b=2" 形式
const list = await getCookies('https://example.com');         // Cookie 数组
```

### DOM 元素创建

```typescript
import { createElement } from '@i.un/libs';

// 创建简单元素
const div = createElement({
  tag: 'div',
  children: 'Hello World',
  styles: { color: 'blue', fontSize: '16px' }
});

// 创建复杂嵌套元素
const card = createElement({
  tag: 'div',
  attrs: { class: 'card' },
  children: [
    {
      tag: 'h2',
      children: '标题',
      styles: { margin: 0 }
    },
    {
      tag: 'p',
      children: '内容描述',
      styles: { color: '#666' }
    }
  ]
});
```

### SVG 创建

```typescript
import { createSvg, createSvgFromUrl } from '@i.un/libs';

// 从字符串创建SVG
const svgString = `
  <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
    <circle cx="50" cy="50" r="40" fill="red" />
  </svg>
`;
const svgElement = createSvg(svgString);

// 从VNode创建SVG
const vnode = {
  type: 'svg',
  props: { width: '100', height: '100' },
  children: [{
    type: 'circle',
    props: { cx: '50', cy: '50', r: '40', fill: 'red' },
    children: null
  }]
};
const svgFromVNode = createSvg(vnode);

// 从URL加载SVG
const svgFromUrl = await createSvgFromUrl('https://example.com/icon.svg');
```

### 缓存包装器

```typescript
import { cacheWrapper } from '@i.un/libs';

// 包装异步函数添加缓存
const fetchUserData = cacheWrapper(async (userId: string) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
}, {
  timeout: 10000,
  timeoutResult: { error: '请求超时' },
  identifier: 'userData'
});

// 第一次调用会执行请求
const user1 = await fetchUserData('123');

// 第二次调用会返回缓存结果
const user2 = await fetchUserData('123'); // 从缓存返回
```

### 深度克隆

```typescript
import { cloneDeep } from '@i.un/libs';

const original = {
  name: 'test',
  items: [1, 2, { nested: true }],
  date: new Date()
};

const cloned = cloneDeep(original);
cloned.items.push(4); // 不会影响原对象
```

### 滚动控制

```typescript
import { scrollToBottom, scrollToBottomIfNeeded } from '@i.un/libs';

const chatContainer = document.getElementById('chat')!;

// 强制滚动到底部
scrollToBottom(chatContainer);

// 智能滚动（只在用户未手动滚动时自动滚动）
scrollToBottomIfNeeded(chatContainer);
```

## 🧪 测试

```bash
npm test
```

## 📝 开发

```bash
# 安装依赖
npm install

# 开发模式
npm run dev

# 构建
npm run build

# 生成文档
npm run doc
```

## 📄 许可证

ISC

## 👨‍💻 作者

zhyswan

## 🔗 相关链接

- [GitHub Repository](https://github.com/zhyswan/libs)
- [NPM Package](https://www.npmjs.com/package/@i.un/libs)
