npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@potmot/command-history

v0.0.1

Published

Lightweight history by command.

Readme

@potmot/command-history

轻量级的命令历史记录库,支持撤销/重做、批量操作和事件监听功能。

特性

  • 📦 批量操作 - 支持批量执行命令与批量 undo/redo
  • 🎯 类型安全 - 完整的 TypeScript 类型支持
  • 🔔 事件系统 - 丰富的事件监听能力

安装

npm install @potmot/command-history
yarn add @potmot/command-history
pnpm add @potmot/command-history

快速开始

前置目标

存在一个待办事项列表,需要实现以下功能:

  • 添加待办事项
  • 删除待办事项
  • 修改待办事项
// 定义待办事项数据类型
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;
};

1. 定义命令类型

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. 创建历史管理器

const history = useCommandHistory<TodoListCommandMap>();

3. 注册命令

注意

  • applyAction 的返回值会作为 revertAction 的参数
  • revertAction 的返回值会作为下次执行时 applyAction 的参数
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. 执行命令

// 执行单个命令
history.executeCommand('add', { text: '学习 TypeScript' });

// 撤销
history.undo();

// 重做
history.redo();

错误处理

库提供了完善的错误处理机制,可以通过 onError 回调来自定义错误处理逻辑。

配置错误处理器

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 提供了详细的错误发生位置信息:

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'};                                            // 清空时

错误处理示例

// 示例 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 };
};

错误常量

所有可能的错误消息定义在 ERROR_MESSAGES 对象中,可直接导入使用:

import { ERROR_MESSAGES } from '@potmot/command-history';

| 常量 | 值 | 触发场景 | |------|-----|---------| | COMMAND_ALREADY_REGISTERED(key) | command ${key} is already registered | 重复注册同名命令 | | COMMAND_NOT_REGISTERED(key) | command ${key} is not registered | 执行或注销未注册的命令 | | EXECUTION_NESTING | Execution does not allow nesting | 在 applyAction/revertAction 中嵌套调用 executeCommandundoredo | | CLEAN_DURING_BATCH | Cannot clean during batch | 在 startBatchstopBatch 之间调用 clean | | UNDO_DURING_BATCH | Cannot undo during batch | 在 startBatchstopBatch 之间调用 undo | | REDO_DURING_BATCH | Cannot redo during batch | 在 startBatchstopBatch 之间调用 redo | | STOP_BATCH_KEY_MISMATCH | stopBatch key is not match | stopBatch 的键与当前批次的键不匹配 |

使用示例:

import { ERROR_MESSAGES } from '@potmot/command-history';

try {
    history.undo();
} catch (error) {
    if (error instanceof Error && error.message === ERROR_MESSAGES.UNDO_DURING_BATCH) {
        console.log('当前在批量操作中,无法撤销');
    }
}

API 文档

CommandMap

命令映射类型,约束命令名和对应的执行和撤销操作参数类型。

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. 每个命令都需要定义 applyActionrevertAction 的类型
  3. applyAction 的返回值类型应该与 revertAction 的参数类型匹配
  4. revertAction 可以返回 undefinedApplyOptions 类型,用于重做时再次执行的参数

核心方法

useCommandHistory(options?)

创建命令历史管理器的工厂函数。

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)

注册一个命令。

history.registerCommand('commandName', {
  applyAction: (options) => {
    // 执行操作
    // options 类型为 CommandDefinition 的第一个泛型参数
    return revertOptions; // 返回撤销时需要的参数
  },
  revertAction: (revertOptions) => {
    // 撤销操作
    // revertOptions 类型为 CommandDefinition 的第二个泛型参数
    return options; // 可选,返回执行时需要的参数
  }
});

示例:

// 完整类型标注示例
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: 命令注销时触发

示例:

// 注销命令
const removedCommand = history.unregisterCommand('add');
console.log('已移除的命令:', removedCommand);

// 监听命令注销
history.eventBus.on('unregisterCommand', ({key}) => {
    console.log(`命令 ${String(key)} 已注销`);
});

executeCommand(key, options)

执行一个已注册的命令,并自动记录到历史栈。

const revertOptions = history.executeCommand('commandName', { /* 参数 */ });
// revertOptions 是 applyAction 的返回值,用于撤销操作

参数说明:

  • key: 要执行的命令的标识符
  • options: 执行命令时传递给 applyAction 的参数

返回值:

  • ReturnType<CommandMap[Key]['applyAction']> | undefined: applyAction 的返回值,用于撤销操作

类型定义:

// executeCommand 的返回值类型是 applyAction 的返回值
const revertOptions: {id: number} = history.executeCommand('add', {
    text: '学习 TypeScript',
    id: 1,
    index: 0
});

示例:

// 执行命令并获取撤销参数
const revertOptions = history.executeCommand('add', {
    text: '学习 TypeScript'
});
console.log(revertOptions); // { id: 1 }

pushCommand(key, options, revertOptions)

手动推送一个命令到历史栈(不执行 applyAction),适用于已经执行过的操作。

// 假设你已经手动执行了某个操作
const revertOptions = { /* 撤销需要的参数 */ };
history.pushCommand('commandName', options, revertOptions);

参数说明:

  • key: 命令的标识符
  • options: 执行命令时的参数(用于记录历史)
  • revertOptions: 撤销操作需要的参数

使用场景: 当你已经手动执行了某个操作,只想将其记录到历史栈时使用。

示例:

// 假设你已经手动执行了添加操作
const newItem = addTodoItem('手动添加的任务');

// 将操作记录到历史栈
history.pushCommand('add', 
    { text: '手动添加的任务', id: newItem.id },
    { id: newItem.id } // 撤销时需要的参数
);

undo()

撤销上一个操作。

history.undo();

相关事件:

  • beforeUndo: 撤销前触发
  • undo: 撤销后触发

示例:

// 检查是否可以撤销
if (history.canUndo()) {
    history.undo();
}

// 监听撤销事件
history.eventBus.on('undo', (commandData) => {
    console.log('已撤销:', commandData);
});

redo()

重做刚才撤销的操作。

history.redo();

相关事件:

  • beforeRedo: 重做前触发
  • redo: 重做后触发

示例:

// 检查是否可以重做
if (history.canRedo()) {
    history.redo();
}

// 监听重做事件
history.eventBus.on('redo', (commandData) => {
    console.log('已重做:', commandData);
});

canUndo(): boolean

检查是否可以撤销(当 undoStack 不为空且当前不在执行操作中时返回 true)。

if (history.canUndo()) { /* ... */ }

示例:

if (history.canUndo()) {
    console.log('可以撤销');
    history.undo();
}

canRedo(): boolean

检查是否可以重做(当 redoStack 不为空且当前不在执行操作中时返回 true)。

if (history.canRedo()) { /* ... */ }

示例:

if (history.canRedo()) {
    console.log('可以重做');
    history.redo();
}

clear()

清空所有历史记录栈(包括 undoStack、redoStack 和 batchStack)。

相关事件:

  • beforeClean: 清空前触发
  • clean: 清空后触发

示例:

// 清空前检查
if (history.getUndoStack().length > 0) {
    history.eventBus.on('beforeClean', () => {
        console.log('即将清空历史记录');
    });
    
    history.clean();
}

// 监听清空事件
history.eventBus.on('clean', () => {
    console.log('历史记录已清空');
});

批量操作

startBatch(key)

开始一个批量操作。

const batchKey = Symbol('batch');
history.startBatch(batchKey);

stopBatch(key)

停止批量操作。

history.stopBatch(batchKey);

executeBatch(key, action)

执行批量操作(同步)。

const batchKey = Symbol('batch');
history.executeBatch(batchKey, () => {
  history.executeCommand('command1', options1);
  history.executeCommand('command2', options2);
  // 这些命令会被作为一个整体记录
});

executeAsyncBatch(key, action)

执行异步批量操作。

const batchKey = Symbol('batch');
await history.executeAsyncBatch(batchKey, async () => {
  await fetchData();
  history.executeCommand('command1', options1);
  history.executeCommand('command2', options2);
});

查询方法

getCommandMap()

获取已注册的命令映射。

getUndoStack()

获取撤销栈。

getRedoStack()

获取重做栈。

getBatchStack()

获取批量操作键栈。

事件系统

通过 eventBus 可以监听各种历史变化事件:

// 监听命令变更(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: 命令注销时

嵌套批量操作

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. 禁止嵌套:在命令的 applyActionrevertAction 中不能再次调用 executeCommandundoredo,否则会抛出错误。

  2. 批次键匹配startBatchstopBatch 必须使用相同的键,且支持嵌套但必须正确配对。

License

MIT