plugins/languages/typescript/skills/async/SKILL.md
TypeScript / JavaScript 异步编程规范,覆盖 async/await 错误处理、AbortController 取消与超时、Promise.all/allSettled/any/try/withResolvers、Streams API、Web Workers、Node worker_threads、scheduler.yield 长任务切片、async iterators、tRPC 类型安全 API、Effect-TS。Use when 编写异步逻辑、并发控制、流式数据、取消请求、超时、API 客户端、Web Worker offloading,或用户提到 "async"、"Promise"、"取消请求"、"AbortController"、"并发"、"超时"、"竞态"、"streaming"、"Web Worker"。
npx skillsauth add lazygophers/ccplugin typescript-asyncInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
本 skill 同时覆盖 JavaScript 项目;示例以 TypeScript 为主,JS 项目去掉类型注解即可。
异步两条铁律:显式取消 + 结构化错误。
typescript-core — 工具链与基线typescript-security — fetch 边界校验// ✅ 多行结构化, try-catch 内必 return await (错误才能进 catch)
async function getUser(id: string): Promise<User> {
try {
const r = await fetch(`/api/users/${id}`);
if (!r.ok) throw new Error(`HTTP ${r.status}: ${r.statusText}`);
const data: unknown = await r.json();
return UserSchema.parse(data); // Zod 边界验证
} catch (e) {
logger.error({ err: e, id }, 'getUser failed');
throw e;
}
}
// ✅ Promise.try (ES2025): 同步异常也进 Promise 链
const p = Promise.try(() => mayThrowSync());
// ❌ 单行
// if (err) return err;
// 超时 — Node 22+ / 现代浏览器内置
async function fetchWithTimeout(url: string, ms = 5000): Promise<Response> {
const signal = AbortSignal.timeout(ms);
return fetch(url, { signal });
}
// 组合多个 signal (ES2024)
const signal = AbortSignal.any([userSignal, timeoutSignal]);
// 竞态:取消过期请求
let inflight: AbortController | null = null;
async function search(q: string) {
inflight?.abort();
inflight = new AbortController();
try {
const r = await fetch(`/api/search?q=${q}`, { signal: inflight.signal });
return await r.json();
} catch (e) {
if ((e as Error).name === 'AbortError') return;
throw e;
}
}
// 统一清理事件监听
function setup(el: HTMLElement) {
const ctrl = new AbortController();
const { signal } = ctrl;
el.addEventListener('click', onClick, { signal });
window.addEventListener('resize', onResize, { signal });
return () => ctrl.abort();
}
// React 19 中取消
useEffect(() => {
const ctrl = new AbortController();
fetchData({ signal: ctrl.signal })
.then(setData)
.catch((err) => { if (!ctrl.signal.aborted) console.error(err); });
return () => ctrl.abort();
}, []);
// Promise.all — 全成功 or 快速失败
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
// Promise.allSettled — 容错并发 (推荐聚合)
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const ok = results
.filter((r): r is PromiseFulfilledResult<Data> => r.status === "fulfilled")
.map((r) => r.value);
// Promise.any — 任一成功 (fallback)
const fastest = await Promise.any([primary(), mirror1(), mirror2()]);
// 限并发 (无外部依赖)
async function pool<T, R>(items: T[], n: number, fn: (i: T) => Promise<R>): Promise<R[]> {
const out: R[] = []; let i = 0;
const workers = Array.from({ length: n }, async () => {
while (i < items.length) {
const idx = i++;
out[idx] = await fn(items[idx]);
}
});
await Promise.all(workers);
return out;
}
// 替代手动 new Promise(...)
const { promise, resolve, reject } = Promise.withResolvers<string>();
element.addEventListener('click', () => resolve('clicked'), { once: true });
const value = await promise;
async function* fetchPages(base: string): AsyncGenerator<Page[]> {
let cursor: string | null = null;
do {
const r = await fetch(`${base}?cursor=${cursor ?? ""}`);
const data = await r.json();
yield data.items;
cursor = data.nextCursor;
} while (cursor);
}
for await (const page of fetchPages("/api/items")) {
for (const item of page) process(item);
}
// 流式 NDJSON 处理
async function* lines(stream: ReadableStream<Uint8Array>) {
const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
let buf = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += value;
const parts = buf.split('\n');
buf = parts.pop()!;
for (const p of parts) if (p) yield JSON.parse(p);
}
if (buf) yield JSON.parse(buf);
}
// TransformStream 管线
const res = await fetch(url);
await res.body!
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TransformStream({ transform(c, ctl) { ctl.enqueue(c.toUpperCase()); } }))
.pipeTo(writableSink);
// Array.fromAsync (ES2025)
const items = await Array.fromAsync(asyncGen(), x => x.id);
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
worker.postMessage({ task: 'parse', payload: data });
worker.addEventListener('message', (e) => console.log(e.data), { once: true });
worker.addEventListener('error', console.error);
// worker.terminate();
import { Worker } from 'node:worker_threads';
const w = new Worker(new URL('./heavy.ts', import.meta.url));
w.postMessage(data);
w.on('message', (r) => console.log(r));
// scheduler.yield() (Chrome 129+, fallback setTimeout 0)
async function processBig<T>(items: T[]) {
for (const item of items) {
work(item);
if ('scheduler' in globalThis && 'yield' in (globalThis as any).scheduler) {
await (globalThis as any).scheduler.yield();
} else {
await new Promise(r => setTimeout(r, 0));
}
}
}
import { initTRPC } from "@trpc/server";
import { z } from "zod";
const t = initTRPC.create();
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.uuid() }))
.query(({ input }) => db.user.findUnique({ where: { id: input.id } })),
createUser: t.procedure
.input(CreateUserSchema)
.mutation(({ input }) => db.user.create({ data: input })),
});
export type AppRouter = typeof appRouter;
// 客户端:完全类型推断
import { Effect, pipe } from "effect";
const getUser = (id: string) =>
pipe(
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`),
catch: () => new NetworkError(),
}),
Effect.flatMap((res) =>
res.ok
? Effect.tryPromise({ try: () => res.json(), catch: () => new ParseError() })
: Effect.fail(new HttpError(res.status)),
),
);
// Effect<unknown, NetworkError | ParseError | HttpError>
上述模式全部适用于 JS 项目,直接去掉类型注解 / 泛型即可。tRPC 与 Effect-TS 强依赖 TS 推断,JS 项目建议改用 OpenAPI + Zod (JSDoc 提供 IDE 提示):
/** @type {(id: string) => Promise<User>} */
export async function getUser(id) {
const r = await fetch(`/api/users/${id}`, { signal: AbortSignal.timeout(5000) });
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return UserSchema.parse(await r.json());
}
| 现象 | 问题 | 严重 |
|------|------|------|
| 顺序 await 独立请求 | 应 Promise.all 并发 | 高 |
| 未 catch / 漏 await | unhandled rejection | 高 |
| try 内 return fetch(...) 不 await | 错误逃出 try | 高 |
| 无超时 | 请求挂死 | 中 |
| 无 AbortController | 组件卸载继续请求 | 中 |
| .then().catch() 链 | async/await 更可读 | 低 |
| Promise.all 中混含可失败任务 | 用 allSettled | 中 |
| 手写 new Promise((res,rej)=>...) | 用 withResolvers | 低 |
| 大数组同步 forEach 阻塞主线程 | 切片 + scheduler.yield | 中 |
| 回调嵌套 | 换 async/await | 高 |
return awaitPromise.all 并发;聚合用 allSettledAbortSignal.timeout{ signal } 统一清理JSON.parse 整文件tools
UI/UX 与布局设计——做界面布局/结构/导航/组件/交互的设计决策。触发:做UI/UX/布局/排版/导航/组件/交互/栅格/响应式/图表选型/字体配对。按媒介路由 HTML/Web、原生 App(iOS/Android/桌面)、CLI、TUI。需后端动态系统不适用;配色/主题/色板走姊妹 skill design-color。
tools
主题与配色设计——做颜色搭配/调色板/主题/品牌色阶/暗模式的设计决策。触发:选配色/调色/主题/色板/品牌色/暗模式/对比度/色盲/UI风格。按媒介路由 HTML/Web(CSS变量)、原生App(平台token)、CLI(ANSI)、TUI(真彩/256/16降级)。保证可访问性(对比度/色盲安全)。需后端动态系统不适用;UI/UX 布局/组件/交互走姊妹 skill design-uiux。
tools
跨任意组件(plugin/skill/agent/command)的验证驱动优化循环纪律 skill。当用户要优化某个已有组件却无明确方向、或要防止改了反而更差(自评乐观偏差 / 多维同改归因失效 / 为凑分加废话膨胀)、或要把一套通用「评分→单变量改→改后验证严格更好才留否则回滚→触顶停」的纪律套到任意组件上时使用。管优化过程本身的纪律(validation gate / ratchet / 独立验证 / 触顶停),不评单组件深度(交 skill-dev),不查插件接线(交 plugin-dev)。仅手动 /optimize-any 触发。
data-ai
两层规则记忆 (基于 .skein/spec)。planning 时 recall 召回相关规则、task finish 后 sediment 沉淀学习 + prune 自动精简过期/重复/断链规则。core 常驻硬规 + recall 按需召回, 经判定门自动写盘 (不逐次问用户)。产出 .skein/spec 下 core/recall 规则文件 + index。另支持空仓 bootstrap 播种规则基线、记忆大面积失效 (大重构/换栈) 时 reconstruct 可逆归档后按项目类型分型重建、maintain 手动体检 (超预算/stale/断链/重复/废弃, --apply 自动修复)、auto-fix (Stop hook 写 .pending-fix 标记 → main 派 skein-specer bg 跑 maintain --apply 全自动修, 断链只报告)。