Skip to content

refurx/gekker

Repository files navigation

🦊 Gekker

What does the fox say?


npm version CI license

English

基于 ArkType 的本地极简事件总线,致力于提供优雅而强大的事件处理能力。

强类型、中间件、错误处理、支持异步、支持模式匹配。

安装

npm install @refurx/gekker
#
bun add @refurx/gekker

快速开始

import { 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() 注册的中间件)"]
Loading

速查表

事件定义

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 的通用路由场景,例如对所有包含 totemplate 字段的事件做统一处理。

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 === EmailSchema

路由方式

bus.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.usebus.after 返回取消订阅函数。不调用 next() 时,本阶段链中的后续中间件也不会被执行。

事件发送

emit() 从 topic + payload 构建事件,自动生成 idcreatedAtemitRaw() 直接传入完整事件对象,适合性能敏感场景。

// 标准路径
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% 的缓存命中率???

About

基于 ArkType 的本地极简事件总线,强类型、中间件、错误处理、模式匹配、异步支持。

Topics

Resources

License

Stars

Watchers

Forks

Contributors