npm package: @potmot/command-history
Description: Lightweight history by command.
Author: potmot
Latest version: 0.0.0

# @potmot/command-history

轻量级的命令历史记录库，支持撤销/重做、批量操作和事件监听功能。

## 特性

- 📦 **批量操作** - 支持批量执行命令与批量 undo/redo
- 🎯 **类型安全** - 完整的 TypeScript 类型支持
- 🔔 **事件系统** - 丰富的事件监听能力

## 安装

```bash
npm install @potmot/command-history
```

```bash
yarn add @potmot/command-history
```

```bash
pnpm add @potmot/command-history
```

## 快速开始


### 前置目标

存在一个待办事项列表，需要实现以下功能：

- 添加待办事项
- 删除待办事项
- 修改待办事项

<details>
    <summary>代码</summary>


```typescript
// 定义待办事项数据类型
type TodoItem = {
    id: number;
    text: string;
};

type TodoItemInput = {
    id?: number;
    text: string;
    index?: number;
};

type TodoItemWithIndex = {
    item: TodoItem;
    index: number;
};

// 定义代办事项数据
const todoList: TodoItem[] = [];
let nextId = 1;

// 定义待办事项操作函数
const addTodoItem = (
    text: string,
    id: number = nextId++,
    index: number = todoList.length,
): TodoItem => {
    const newTodo: TodoItem = {id, text};
    todoList.splice(index, 0, newTodo);
    return newTodo;
};

const removeTodoItem = (id: number): TodoItemWithIndex => {
    for (let i = 0; i < todoList.length; i++) {
        if (todoList[i].id === id) {
            const todoItem = todoList[i];
            todoList.splice(i, 1);
            return {item: todoItem, index: i};
        }
    }
    throw new Error('Todo item not found');
};

const updateTodoItem = (todoItem: TodoItem): TodoItem => {
    const index = todoList.findIndex((item) => item.id === todoItem.id);
    if (index === -1) {
        throw new Error('Todo item not found');
    }
    const oldTodoItem = todoList[index];
    if (!oldTodoItem) {
        throw new Error('Todo item not found');
    }
    todoList[index] = todoItem;
    return oldTodoItem;
};
```
</details>

### 1. 定义命令类型

```typescript
import { useCommandHistory, type CommandDefinition } from '@potmot/command-history';

// 定义你的命令映射类型
// CommandDefinition<执行操作参数，撤销操作参数>
type TodoListCommandMap = {
    add: CommandDefinition<TodoItemInput, {id: number}>;
    remove: CommandDefinition<{id: number}, TodoItemWithIndex>;
    update: CommandDefinition<TodoItem, TodoItem>;
};
```

### 2. 创建历史管理器

```typescript
const history = useCommandHistory<TodoListCommandMap>();
```

### 3. 注册命令

**注意**：
- `applyAction` 的返回值会作为 `revertAction` 的参数
- `revertAction` 的返回值会作为下次执行时 `applyAction` 的参数

```typescript
history.registerCommand('add', {
    applyAction: ({text, id, index}) => {
        const item = addTodoItem(text, id, index);
        return {id: item.id};
    },
    revertAction: ({id}) => {
        const {item, index} = removeTodoItem(id);
        return {text: item.text, id: item.id, index};
    },
});

history.registerCommand('remove', {
    applyAction: ({id}) => {
        return removeTodoItem(id);
    },
    revertAction: ({item, index}) => {
        const newItem = addTodoItem(item.text, item.id, index);
        return {id: newItem.id};
    },
});

history.registerCommand('update', {
    applyAction: (newItem) => {
        const oldItem = updateTodoItem(newItem);
        return {id: oldItem.id, text: oldItem.text};
    },
    revertAction: (oldItem) => {
        const newItem = updateTodoItem(oldItem);
        return {id: newItem.id, text: newItem.text};
    },
});
```

### 4. 执行命令

```typescript
// 执行单个命令
history.executeCommand('add', { text: '学习 TypeScript' });

// 撤销
history.undo();

// 重做
history.redo();
```

## 错误处理

库提供了完善的错误处理机制，可以通过 `onError` 回调来自定义错误处理逻辑。

### 配置错误处理器

```typescript
import { useCommandHistory, type ErrorHandler, type ErrorContext } from '@potmot/command-history';

// 自定义错误处理函数
const errorHandler: ErrorHandler<TodoListCommandMap> = (error, context) => {
    console.error('发生错误:', error);
    console.error('错误上下文:', context);
    
    // 根据错误类型进行不同处理
    switch (context.type) {
        case 'execute':
            console.error(`执行命令 ${String(context.key)} 时出错，操作类型：${context.operation}`);
            break;
        case 'register':
            console.error(`注册命令 ${String(context.key)} 时出错`);
            break;
        case 'unregister':
            console.error(`注销命令 ${String(context.key)} 时出错`);
            break;
        case 'batch':
            console.error(`批量操作出错，操作类型：${context.operation}`);
            break;
        case 'undo':
            console.error('撤销操作出错');
            break;
        case 'redo':
            console.error('重做操作出错');
            break;
        case 'clean':
            console.error('清空历史记录时出错');
            break;
    }
    
    // 返回 stillThrow: false 可以阻止错误继续抛出
    // 返回 undefined 或 stillThrow: true 会让错误继续抛出
    return { stillThrow: false };
};

const history = useCommandHistory<TodoListCommandMap>({
    onError: errorHandler
});
```

### 错误上下文类型

`ErrorContext` 提供了详细的错误发生位置信息：

```typescript
type ErrorContext<CommandMap> =
    | {type: 'execute', key: Key, operation: 'apply' | 'revert'}  // 执行命令时
    | {type: 'register', key: Key}                                // 注册命令时
    | {type: 'unregister', key: Key}                              // 注销命令时
    | {type: 'batch', operation: 'start' | 'stop' | 'execute'}    // 批量操作时
    | {type: 'undo' | 'redo'}                                     // 撤销/重做时
    | {type: 'clean'};                                            // 清空时
```

### 错误处理示例

```typescript
// 示例 1: 记录错误但不中断程序
history.registerCommand('add', {
    applyAction: ({text, id, index}) => {
        // 假设这里发生了错误
        throw new Error('添加失败');
    },
    revertAction: ({id}) => {
        // ...
    },
});

// 如果配置了 onError 并返回 { stillThrow: false }
// 错误会被捕获但不会中断程序执行

// 示例 2: 根据错误类型决定是否抛出
const selectiveErrorHandler: ErrorHandler<TodoListCommandMap> = (error, context) => {
    // 对于 undo/redo 错误，不抛出（用户无感知）
    if (context.type === 'undo' || context.type === 'redo') {
        console.warn('撤销/重做失败:', error);
        return { stillThrow: false };
    }
    
    // 其他错误继续抛出
    return { stillThrow: true };
};
```

## API 文档

### CommandMap

命令映射类型，约束命令名和对应的执行和撤销操作参数类型。

```typescript
type CommandDefinition<ApplyOptions, RevertOptions = ApplyOptions> = {
    applyAction: (options: ApplyOptions) => RevertOptions;
    revertAction: (options: RevertOptions) => ApplyOptions | undefined | void;
};

type CustomCommandMap = Record<string, CommandDefinition<any>>;
```

**注意事项：**
1. `CommandMap` 必须继承自 `CustomCommandMap`
2. 每个命令都需要定义 `applyAction` 和 `revertAction` 的类型
3. `applyAction` 的返回值类型应该与 `revertAction` 的参数类型匹配
4. `revertAction` 可以返回 `undefined` 或 `ApplyOptions` 类型，用于重做时再次执行的参数

### 核心方法

#### `useCommandHistory(options?)`

创建命令历史管理器的工厂函数。

```typescript
import { useCommandHistory, type CommandHistoryOptions } from '@potmot/command-history';

const history = useCommandHistory<MyCommandMap>({
    onError: (error, context) => {
        console.error('错误:', error, context);
        return { stillThrow: false }; // 阻止错误抛出
    }
});
```

**参数说明：**
- `options`: 可选配置对象
  - `onError`: 错误处理函数，类型为 `ErrorHandler<CommandMap>`

**返回值：**
- `CommandHistory<CommandMap>`: 命令历史管理器实例

#### `registerCommand(key, options)`

注册一个命令。

```typescript
history.registerCommand('commandName', {
  applyAction: (options) => {
    // 执行操作
    // options 类型为 CommandDefinition 的第一个泛型参数
    return revertOptions; // 返回撤销时需要的参数
  },
  revertAction: (revertOptions) => {
    // 撤销操作
    // revertOptions 类型为 CommandDefinition 的第二个泛型参数
    return options; // 可选，返回执行时需要的参数
  }
});
```

**示例：**

```typescript
// 完整类型标注示例
history.registerCommand('add', {
    applyAction: ({text, id, index}): {id: number} => {
        const item = addTodoItem(text, id, index);
        return {id: item.id};
    },
    revertAction: ({id}): {text: string; id: number; index?: number} | void => {
        const {item, index} = removeTodoItem(id);
        return {text: item.text, id: item.id, index};
    },
});
```

#### `unregisterCommand(key)`

注销一个已注册的命令。

**相关事件：**
- `unregisterCommand`: 命令注销时触发

**示例：**

```typescript
// 注销命令
const removedCommand = history.unregisterCommand('add');
console.log('已移除的命令:', removedCommand);

// 监听命令注销
history.eventBus.on('unregisterCommand', ({key}) => {
    console.log(`命令 ${String(key)} 已注销`);
});
```

#### `executeCommand(key, options)`
执行一个已注册的命令，并自动记录到历史栈。

```typescript
const revertOptions = history.executeCommand('commandName', { /* 参数 */ });
// revertOptions 是 applyAction 的返回值，用于撤销操作
```

**参数说明：**
- `key`: 要执行的命令的标识符
- `options`: 执行命令时传递给 `applyAction` 的参数

**返回值：**
- `ReturnType<CommandMap[Key]['applyAction']> | undefined`: `applyAction` 的返回值，用于撤销操作

**类型定义：**

```typescript
// executeCommand 的返回值类型是 applyAction 的返回值
const revertOptions: {id: number} = history.executeCommand('add', {
    text: '学习 TypeScript',
    id: 1,
    index: 0
});
```

**示例：**

```typescript
// 执行命令并获取撤销参数
const revertOptions = history.executeCommand('add', {
    text: '学习 TypeScript'
});
console.log(revertOptions); // { id: 1 }
```

#### `pushCommand(key, options, revertOptions)`
手动推送一个命令到历史栈（不执行 applyAction），适用于已经执行过的操作。

```typescript
// 假设你已经手动执行了某个操作
const revertOptions = { /* 撤销需要的参数 */ };
history.pushCommand('commandName', options, revertOptions);
```

**参数说明：**
- `key`: 命令的标识符
- `options`: 执行命令时的参数（用于记录历史）
- `revertOptions`: 撤销操作需要的参数

**使用场景：**
当你已经手动执行了某个操作，只想将其记录到历史栈时使用。

**示例：**

```typescript
// 假设你已经手动执行了添加操作
const newItem = addTodoItem('手动添加的任务');

// 将操作记录到历史栈
history.pushCommand('add', 
    { text: '手动添加的任务', id: newItem.id },
    { id: newItem.id } // 撤销时需要的参数
);
```

#### `undo()`
撤销上一个操作。

```typescript
history.undo();
```

**相关事件：**
- `beforeUndo`: 撤销前触发
- `undo`: 撤销后触发

**示例：**

```typescript
// 检查是否可以撤销
if (history.canUndo()) {
    history.undo();
}

// 监听撤销事件
history.eventBus.on('undo', (commandData) => {
    console.log('已撤销:', commandData);
});
```

#### `redo()`
重做刚才撤销的操作。

```typescript
history.redo();
```

**相关事件：**
- `beforeRedo`: 重做前触发
- `redo`: 重做后触发

**示例：**

```typescript
// 检查是否可以重做
if (history.canRedo()) {
    history.redo();
}

// 监听重做事件
history.eventBus.on('redo', (commandData) => {
    console.log('已重做:', commandData);
});
```

#### `canUndo(): boolean`
检查是否可以撤销（当 undoStack 不为空且当前不在执行操作中时返回 true）。

```typescript
if (history.canUndo()) { /* ... */ }
```

**示例：**

```typescript
if (history.canUndo()) {
    console.log('可以撤销');
    history.undo();
}
```

#### `canRedo(): boolean`
检查是否可以重做（当 redoStack 不为空且当前不在执行操作中时返回 true）。

```typescript
if (history.canRedo()) { /* ... */ }
```

**示例：**

```typescript
if (history.canRedo()) {
    console.log('可以重做');
    history.redo();
}
```

#### `clear()`

清空所有历史记录栈（包括 undoStack、redoStack 和 batchStack）。

**相关事件：**
- `beforeClean`: 清空前触发
- `clean`: 清空后触发

**示例：**

```typescript
// 清空前检查
if (history.getUndoStack().length > 0) {
    history.eventBus.on('beforeClean', () => {
        console.log('即将清空历史记录');
    });
    
    history.clean();
}

// 监听清空事件
history.eventBus.on('clean', () => {
    console.log('历史记录已清空');
});
```

### 批量操作

#### `startBatch(key)`
开始一个批量操作。

```typescript
const batchKey = Symbol('batch');
history.startBatch(batchKey);
```

#### `stopBatch(key)`
停止批量操作。

```typescript
history.stopBatch(batchKey);
```

#### `executeBatch(key, action)`
执行批量操作（同步）。

```typescript
const batchKey = Symbol('batch');
history.executeBatch(batchKey, () => {
  history.executeCommand('command1', options1);
  history.executeCommand('command2', options2);
  // 这些命令会被作为一个整体记录
});
```

#### `executeAsyncBatch(key, action)`
执行异步批量操作。

```typescript
const batchKey = Symbol('batch');
await history.executeAsyncBatch(batchKey, async () => {
  await fetchData();
  history.executeCommand('command1', options1);
  history.executeCommand('command2', options2);
});
```

### 查询方法

#### `getCommandMap()`
获取已注册的命令映射。

#### `getUndoStack()`
获取撤销栈。

#### `getRedoStack()`
获取重做栈。

#### `getBatchStack()`
获取批量操作键栈。

### 事件系统

通过 `eventBus` 可以监听各种历史变化事件：

```typescript
// 监听命令变更（apply: 执行，revert: 撤销，push: 手动推送）
history.eventBus.on('change', (data) => {
  console.log('命令变更:', data.type, data.key, data);
});

// 监听撤销
history.eventBus.on('undo', (data) => {
  console.log('撤销操作:', data);
});

// 监听重做
history.eventBus.on('redo', (data) => {
  console.log('重做操作:', data);
});

// 监听批量操作开始/结束
history.eventBus.on('batchStart', () => { /* ... */ });
history.eventBus.on('batchStop', () => { /* ... */ });

// 其他可用事件：
// - beforeChange: 命令执行前（包含 type: 'apply' | 'revert' | 'push'）
// - beforeUndo: 撤销前
// - beforeRedo: 重做前
// - beforeClean: 清空前
// - registerCommand: 命令注册时
// - unregisterCommand: 命令注销时
```

### 嵌套批量操作

```typescript
const outerBatch = Symbol('outer');
const innerBatch = Symbol('inner');

history.executeBatch(outerBatch, () => {
  history.executeCommand('command1', options1);
  
  // 嵌套批量操作
  history.executeBatch(innerBatch, () => {
    history.executeCommand('command2', options2);
  });
  
  history.executeCommand('command3', options3);
});

// 撤销时会作为一个包含 3 个命令的整体批次处理
history.undo();
```

## 注意事项

1. **禁止嵌套**：在命令的 `applyAction` 或 `revertAction` 中不能再次调用 `executeCommand`、`undo` 或 `redo`，否则会抛出错误。

2. **批次键匹配**：`startBatch` 和 `stopBatch` 必须使用相同的键，且支持嵌套但必须正确配对。

## License

MIT