What does the fox say?
基于 ArkType 的本地极简事件总线,致力于提供优雅而强大的事件处理能力。
强类型、中间件、错误处理、支持异步、支持模式匹配。
npm install @refurx/gekker
# 或
bun add @refurx/gekkerimport { EventBus, defineEvent } from "gekker"
import { type } from "arktype"
// 定义事件的 ArkType Schema
const UserCreatedSchema = type({
name: "string",
age: "number"
})
const UserCreated = defineEvent("user.created", UserCreatedSchema)
// 创建总线
const bus = new EventBus()
// 订阅事件
const unsub = bus.on(UserCreated, (event) => {
console.log(`${event.name} (${event.age})`)
})
// 发送事件
await bus.emit("user.created", { name: "Alice", age: 30 })
// → Alice (30)
// 取消订阅
unsub()Gekker 使用三阶段管道分发事件:
flowchart LR
A["bus.emit()"] --> B["构建事件 + __meta"]
B --> C["1. before 阶段<br/>(use() 注册的中间件)"]
C --> D{"next()?"}
D -->|"否"| E["短路:跳过后续阶段"]
D -->|"是"| F["2. match 阶段<br/>(on() 注册的处理器)"]
F --> G{"匹配?"}
G -->|"是"| H["执行 handler"]
G -->|"否"| I["跳过"]
H --> I
I --> J["3. after 阶段<br/>(after() 注册的中间件)"]
defineEvent 有两种用法,第二个参数可以直接传入一个 ArkType Schema,也可以直接传入用于生成 ArkType Schema 的模板对象。
// 写法一:直接传入 ArkType Schema
const UserCreatedSchema = type({
name: "string",
age: "number"
})
const UserCreated = defineEvent("user.created", UserCreatedSchema)
// 写法二:直接传入模板对象
const UserCreated = defineEvent("user.created", {
name: "string",
age: "number"
})出于可读性的考虑,更推荐把 ArkType Schema 定义和事件定义分开,然后使用 defineEvent 的第一种写法进行组合。
此外,事件定义时还可以传入用于扩展元数据字段的数据模型作为第三个参数:
const UserCreated = defineEvent("user.created", UserCreatedSchema, {
source: "string"
})
// 当然也可以用 ArkType Schema 来定义元数据
const UserCreatedMetaSchema = type({
source: "string"
})
const UserCreated = defineEvent("user.created", UserCreatedSchema, UserCreatedMetaSchema)definePattern 用于创建可复用的部分匹配模式——不锁定 __meta.topic,只要 payload 字段匹配即命中。适合跨 topic 的通用路由场景,例如对所有包含 to 和 template 字段的事件做统一处理。
const EmailLike = definePattern({ to: "string", template: "string" })
bus.on(EmailLike, (event) => {
// 所有 { to: string, template: string } 的事件都会走到这里
console.log(`${event.template} → ${event.to}`)
})结构等价的 pattern 会共享同一个编译后 schema 实例,内存友好:
const a = definePattern({ to: "string", template: "string" })
const b = definePattern({ template: "string", to: "string" }) // 字段顺序无关
console.log(a === b) // true与 defineEvent 类似,definePattern 也接受预构建的 ArkType schema:
const EmailSchema = type({ to: "string", template: "string" })
const EmailLike = definePattern(EmailSchema) // EmailLike === EmailSchemabus.on() 接受三种形式的匹配条件,按性能由高到低排列:
// 1. isTopic(topic) — 纯字符串比较,无 ArkType 开销
bus.on(isTopic("user.created"), (event) => {
// 等价于 event.__meta.topic === "user.created"
// 需要注意的是这样的写法没有类型保护,event 的类型无法自动推断
})
// 2. ArkType Schema — topic 字面量不匹配时零开销跳过,命中后才走 allows()
bus.on(UserCreated, (event) => {
// event 的类型自动推断
// 最推荐的写法,能够推导出完整的事件类型
})
// 3. 内联模板对象 — 首次编译后结果缓存,后续调用复用
bus.on({ to: "string", template: "string" }, (event) => {
// 相当于自动调用 definePattern 再传入
// 这里也可以传入 ArkType Schema 实例,效果同上
})
// 4. 自定义 filter 函数 — 同步或异步均可,灵活但无优化路径
bus.on(
(event) => event.__meta.topic.startsWith("user."),
(event) => {
// 所有 user.* topic 事件
}
)Gekker 采用三阶段管道架构,将事件处理分为三个独立阶段,执行顺序固定(不受注册顺序影响):
| 阶段 | 注册方法 | 说明 |
|---|---|---|
| before | bus.use() |
预处理中间件,始终最先执行。不调用 next() 可短路后续阶段 |
| match | bus.on() |
类型化处理器,仅在 schema / filter 匹配时执行 |
| after | bus.after() |
后处理中间件,在 match 阶段完成后最后执行 |
// before 阶段:预处理
bus.use(async (event, next) => {
console.time(event.__meta.id)
await next()
console.timeEnd(event.__meta.id)
})
// 短路后续阶段
bus.use(async (event, next) => {
if (event.__meta.topic !== "public") return // 不调 next(),match 和 after 都不执行
return next()
})
// match 阶段:匹配处理
bus.on(UserCreated, (event) => {
console.log(`${event.name} (${event.age})`)
})
// after 阶段:后处理(清理、审计等)
bus.after(async (event, next) => {
console.log("事件处理完成:", event.__meta.id)
return next()
})bus.use 和 bus.after 返回取消订阅函数。不调用 next() 时,本阶段链中的后续中间件也不会被执行。
emit() 从 topic + payload 构建事件,自动生成 id 和 createdAt;emitRaw() 直接传入完整事件对象,适合性能敏感场景。
// 标准路径
await bus.emit("user.created", { name: "Alice", age: 30 })
// 带元数据扩展
await bus.emit("order.placed", { amount: 99 }, { source: "web" })
// 性能路径:若已提供 __meta.id 和 __meta.createdAt,零拷贝复用对象
await bus.emitRaw({
__meta: { topic: "user.created", id: myId(), createdAt: Date.now() },
name: "Alice",
age: 30
})Gekker 提供一个总线级的 onError 注入来方便地捕获和处理事件处理器中的错误。
const bus = new Gekker({
onError: (error, event) => {
// 这里注入一个 console 用于打印错误信息到控制台中
console.error("Event error:", error)
}
})默认值就是 console.error。handler / filter 中抛出的异常都会被捕获并交给 onError,链路继续执行,emit() / emitRaw() 返回的 Promise 永不 reject。
createBus<E>() 将总线限定在特定的事件联合类型内,emitRaw() 仅接受声明过的事件类型。
type AppEvent = typeof UserCreated.infer | typeof OrderPlaced.infer
const bus = createBus<AppEvent>()
bus.on(UserCreated, (e) => {
// e 的类型自动推断为 UserCreatedEvent
console.log(e.name) // ✅
})
bus.emitRaw({ __meta: { topic: "unknown" }, foo: "bar" }) // ❌ 类型错误以下结果来自 bun run scripts/benchmark.ts(100K 迭代 / 每场景,取 5 轮中位数)。测试用机为 AMD Ryzen 7 8845H,Bun 1.3.14。
| Scenario | ops/s | ns/op |
|---|---|---|
| emit(), no subscribers | 5,995,085 (6.0M) | 167 |
| emitRaw(), no subscribers | 49,042,999 (49.0M) | 20 |
| isTopic, match | 3,126,311 (3.1M) | 320 |
| isTopic, mismatch | 6,306,065 (6.3M) | 159 |
| on(schema), match | 3,930,966 (3.9M) | 254 |
| on(schema), mismatch | 3,904,054 (3.9M) | 256 |
| on(pattern), match | 6,589,350 (6.6M) | 152 |
| on(pattern), mismatch | 3,072,653 (3.1M) | 325 |
| on(filter), match | 9,258,471 (9.3M) | 108 |
| on(filter), mismatch | 6,346,838 (6.3M) | 158 |
| 10 handlers, 5 match | 131,811 (0.1M) | 7,587 |
| chain depth 5 | 3,079,158 (3.1M) | 325 |
| 20 handlers, 1 match | 656,973 (0.7M) | 1,522 |
| 20 handlers, 0 match | 1,002,176 (1.0M) | 998 |
| defineEvent() | 17,054 (0.02M) | 58,636 |
| definePattern() (cached) | 1,686,908 (1.7M) | 593 |
说实话测了很多次波动挺大的,而且这玩意用起来真正的负载都在 handler 里面,仅供图一乐参考一下。
- @Tunanodra 提供了在我鞭策 Claude Code 干活的时候和我唠嗑的情绪价值,以及和我讨论了很久这玩意的原型设计。
- Claude Code 真干活的,所有的测试和注释以及代码评审(还有相当一部分修复工作)都是 Claude Code 做的,氛围感这一块。
- DeepseekV4 Pro 什么叫 96% 的缓存命中率???