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

@refurx/gekker

v2.1.3

Published

Type-safe async event bus backed by ArkType

Readme

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

速查表

事件定义

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% 的缓存命中率???