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

@composy/event-core

v0.0.2

Published

LDesign 事件系统 - 框架无关的核心模块

Readme

@composy/event-core

审计增强后的运行约定

  • maxHistory 必须是 0 到 100000 之间的有限整数;NaN、无穷大、小数和负数会抛出 ConfigError。0 表示关闭历史记录。
  • 历史记录保存的是冻结快照,不会被调用方后续修改原始 payload 反向污染。
  • handler 默认采用 fail-fast,保持顺序派发和原始错误传播;需要隔离单个 handler 时显式设置 failurePolicy: 'isolate'。onError 只作为诊断 sink,其自身异常会被吞并,不会覆盖业务错误。
  • EventBus、EventChannel、EventChannelManager 和 EventScheduler 销毁后进入终态;重复 destroy() 安全,继续注册或派发会抛出 LifecycleError。
  • 通配符默认兼容历史输入并归一化空片段;需要严格校验时使用 patternMode: 'strict'(或 strictPatterns: true)。分隔符不能为空。
  • EventChannelManager 对同名 channel 的 busOptions 做一致性校验,冲突配置会抛出 ConfigError,不会静默复用错误配置。
  • on、once、prependListener、observe 以及频道/命名空间订阅均支持 EventSubscriptionOptions.signal;已取消的 signal 不会注册,后续 abort 只移除当前订阅。
  • 事件名必须是非空字符串;getHistory('') 和 { event: '' } 仍按精确条件查询,不会误返回全部历史。
  • bridge 的 filter、transform 和目标派发都纳入 failurePolicy 与 onError 诊断;调度器遇到未知 mode 会抛出 ConfigError,不会静默丢弃事件。
  • Engine 插件按 Engine 实例隔离 installation lease;默认复用 state 中的 Bus,自动创建的 Bus 由插件拥有并在卸载时销毁,外部 Bus 只借用。安装失败会回滚 state 和资源。

isEventBus() 使用跨 bundle 的稳定 brand,而不是 instanceof,适用于重复打包和 iframe 场景。可从根入口导入 ConfigError、LifecycleError、isEventBus 与 EVENT_CORE_PACKAGE_VERSION。

@composy/event-core 是 packages/event 的框架无关核心层,负责事件总线、频道隔离、过滤规则、命名空间、事件调度和引擎插件接入。

安装

pnpm add @composy/event-core

核心能力

| 模块 | 能力说明 | | ------------------------- | ------------------------------------------------------------------------------------------------------ | | EventBus | 类型安全的发布/订阅、AbortSignal 生命周期、once、prependListener、通配符监听、事件历史、观察者扩展 | | EventChannel | 独立频道和跨频道桥接,支持过滤与异步转换 | | EventFilter | 全局/精确/通配符三级过滤规则,发送前拦截,支持按事件或全部清理 | | EventNamespace | 基于前缀的事件隔离,自动管理生命周期 | | EventScheduler | delay/throttle/debounce/batch 四种调度策略 | | createEventEnginePlugin | 与 @composy/engine-core 风格上下文对接的插件 |

模块结构

src/
├── core/                        # 核心运行时模块
│   ├── eventBus.ts              # 事件总线(发布/订阅/通配符/历史/观察器)
│   ├── eventChannel.ts          # 频道隔离与跨频道桥接
│   ├── eventFilter.ts           # 事件发送前三级过滤
│   ├── eventNamespace.ts        # 基于前缀的命名隔离
│   ├── eventScheduler.ts        # 延迟/防抖/节流/批量调度
│   └── internal/                # 内部实现(不建议外部依赖)
│       ├── eventHistoryStore.ts # 有界环形历史存储
│       └── eventPatternMatcher.ts # 通配符模式匹配器
├── engine/                      # 引擎插件桥接层
│   ├── plugin.ts                # createEventEnginePlugin 工厂
│   └── types.ts                 # 引擎插件类型定义
├── types/                       # 全部公开 TypeScript 类型
│   └── index.ts
└── index.ts                     # 包统一入口

快速开始

import { EventBus, EventFilter, EventScheduler } from '@composy/event-core'

interface AppEvents {
  'user:login': { id: number; name: string }
  'search:query': string
}

// 创建事件总线
const bus = new EventBus<AppEvents>({ maxHistory: 50 })

// 创建过滤器和调度器
const filter = new EventFilter(bus)
const scheduler = new EventScheduler(bus)

// 添加过滤规则:只允许非空载荷通过
filter.addRule('user:*', (_, payload) => Boolean(payload))

// 注册节流调度:搜索输入 200ms 节流
scheduler.register('search:query', { mode: 'throttle', wait: 200, trailing: true })

// 订阅事件
bus.on('user:login', (payload, context) => {
  console.info(`${context.event}: ${payload.name}`)
})

// 订阅可以绑定调用方生命周期;abort 只移除这一条监听
const controller = new AbortController()
bus.on('user:login', () => console.info('可取消审计'), {
  signal: controller.signal,
})
controller.abort()

// 将高优先级监听器插入同一事件队列头部
bus.prependListener('user:login', () => console.info('先执行'))

// 发布事件
await bus.emit('user:login', { id: 1, name: 'LDesign' })

// 调度器发布(自动节流)
await scheduler.emit('search:query', 'event bus')

// 查询实际会被触发的监听器数量(包含匹配的通配符和 once)
const listeners = bus.listenerCount('user:login')

通配符监听

// 注册通配符监听器
bus.on('user:*', (payload, context) => {
  // context.event 包含真实事件名(如 'user:login')
  console.info(`捕获: ${context.event}`)
})

// 注册多层通配符
bus.on('order:**', (payload, context) => {
  console.info(`订单事件: ${context.event}`)
})

// 一次性通配符
bus.once('system:*', () => {
  console.info('系统事件首次触发')
})

当 EventBus 携带具体 EventMap 泛型时,通配符 handler 的 payload 会自动收窄为命中事件载荷的联合类型:user:* 只包含单层 user:* 事件,user:** 可包含多层事件。

* 只匹配一个非空层级:user:* 可匹配 user:login,不会匹配 user:profile:update。** 匹配零个或多个层级:order:** 可匹配 order、order:create 和 order:item:created。

事件过滤

const filter = new EventFilter(bus)

// 全局过滤(所有事件生效)
filter.addRule('*', (event, payload) => {
  return payload !== undefined
})

// 通配符模式过滤
filter.addRule('user:*', (_, payload) => {
  return (payload as { id: number }).id > 0
})

// 精确事件过滤
filter.addRule('user:login', (_, payload) => {
  return (payload as { name: string }).name.length > 0
})

// 检查事件是否通过过滤
const pass = await filter.check('user:login', { id: 1, name: 'LDesign' })

// 通过过滤后发送事件
await filter.emit('user:login', { id: 1, name: 'LDesign' })

// 清理单个作用域或全部规则;不会影响 bus 的监听器和历史
filter.removeRules('user:*')
filter.removeAllRules()

事件调度

const scheduler = new EventScheduler(bus)

// 延迟调度:500ms 后发送
scheduler.register('save', { mode: 'delay', wait: 500 })

// 防抖调度:最后一次调用后 300ms 发送;持续输入最多等待 1 秒
scheduler.register('search', { mode: 'debounce', wait: 300, maxWait: 1000 })

// 节流调度:200ms 内最多发送一次
scheduler.register('scroll', { mode: 'throttle', wait: 200, leading: true })

// 批量调度:攒够 10 条或 1s 后批量发送
scheduler.register('batch-save', { mode: 'batch', wait: 1000, maxBatchSize: 10 })

// 立即刷新所有待发送事件
await scheduler.flushAll()

// 丢弃 search 当前待发送值,但保留 debounce 注册
scheduler.cancel('search')

频道与桥接

import { EventChannelManager } from '@composy/event-core'

const manager = new EventChannelManager()
const source = manager.getChannel('source')
const target = manager.getChannel('target')

// 监听目标频道
target.on('target:update', payload => {
  console.info('收到桥接:', payload)
})

// 创建桥接:source:sync -> target:update,过滤 + 异步转换
source.bridge(target, {
  sourceEvent: 'source:sync',
  targetEvent: 'target:update',
  filter: payload => (payload as { id: number }).id > 1,
  async transform(payload, context) {
    return { id: (payload as { id: number }).id, sourceEvent: context.event }
  },
})

// 源频道发送事件,自动桥接到目标
await source.emit('source:sync', { id: 2 })

bridge() 会从 sourceEvent 和 targetEvent 推导 filter/transform 的源、目标载荷类型;省略 targetEvent 时,两侧频道必须声明同名事件。

命名空间

import { EventNamespace } from '@composy/event-core'

// 创建命名空间:自动添加 'user:' 前缀
const userNs = new EventNamespace(bus, { name: 'user' })

userNs.on('login', handler) // 实际监听 'user:login'
userNs.emit('login', data) // 实际发送 'user:login'

// 销毁命名空间时自动取消所有监听器
userNs.destroy()

引擎插件生命周期

import { createEventEnginePlugin, eventStateKeys } from '@composy/event-core'

// 创建插件;默认由插件创建并拥有 EventBus
const plugin = createEventEnginePlugin()

// 安装到引擎:优先复用 state 中已有 bus,其次使用 options.bus,最后自动创建
await plugin.install(engine)

// 从引擎 state 获取共享 bus
const bus = engine.state?.get?.(eventStateKeys.BUS)
if (bus) {
  await bus.emit('system:ready')
}

// 卸载插件:自动创建的 bus 会销毁,插件写入的 state 会删除
await plugin.uninstall(engine)

插件安装/卸载遵循以下资源边界:

  • state[eventStateKeys.BUS] 已存在 EventBus 时只复用,不会在卸载时销毁或删除它。
  • 通过 createEventEnginePlugin({ bus }) 注入外部 bus 时,插件负责登记 state,但默认不销毁外部 bus;需要显式释放时使用 destroyBusOnUninstall: true。
  • 插件自动创建的 bus 由插件拥有,卸载时会调用 destroy(),并删除插件写入的 state。
  • 安装完成后发布 event:installed,卸载完成后发布 event:uninstalled;日志器存在时同步写入安装日志。
  • install() 与 uninstall() 支持直接传入 engine,也支持传入 { engine } 包装上下文。

事件历史查询

// 获取全部历史
const all = bus.getHistory()

// 按事件名过滤
const userEvents = bus.getHistory('user:login')

// 按通配符模式过滤
const userPattern = bus.getHistory({ pattern: 'user:*' })

// 按时间范围查询,可与 event 或 pattern 组合(范围两端均包含)
const recentLogins = bus.getHistory({
  event: 'user:login',
  fromTimestamp: Date.now() - 60_000,
  toTimestamp: Date.now(),
})

// 清空历史
bus.clearHistory()

// 回放历史事件(不会二次写入历史)
await bus.replay('user:login')

观察器

// 注册观察器(用于 DevTools/调试面板)
const stop = bus.observe(change => {
  if (change.type === 'emit') {
    console.info(`[${change.event}] 已发送`)
  } else if (change.type === 'error') {
    console.error(`[${change.event}] 错误:`, change.error)
  }
})

// 取消观察
stop()

类型导出

// 全部类型从 @composy/event-core 导入
import type {
  EventMap,
  EventHandler,
  EventHandlerContext,
  EventObserver,
  EventRecord,
  EventBusOptions,
  EventFilterFn,
  ScheduleOptions,
  EventChannelOptions,
  EventBridgeRule,
  EventSubscriptionOptions,
} from '@composy/event-core'

// 引擎插件类型
import type {
  EventEnginePlugin,
  EventEnginePluginOptions,
  EventEngineLike,
} from '@composy/event-core/engine'

设计要点

  • 双注册表架构:精确事件走 O(1) Map 查找,通配符事件走预编译 matcher 遍历,热路径只扫描精确表
  • 模式匹配缓存:每个预编译 matcher 使用 256 项 LRU 缓存近期事件结果,避免高频重复事件反复拆分和递归匹配
  • 有界环形存储:事件历史使用环形数组,避免高频 shift() 的线性搬移开销
  • 观察器模式:DevTools、调试面板和 Vue 响应式适配层不必轮询,通过 observer 实时推送
  • 统一错误上报:handler/observer/scheduler/bridge/plugin 错误通过 onError 统一暴露
  • 生命周期隔离:AbortSignal 只取消当前订阅,不会清理同事件的其他监听器
  • 快照隔离:历史 payload 通过 structuredClone 或循环安全降级复制与发送方解耦,Map/Set 也会复制条目
  • 零 any 类型:全部类型使用 unknown 上界,保持类型安全
  • 框架无关:纯 TypeScript 实现,可直接在 vanilla/React/Vue/Svelte 等任意环境使用

验证命令

pnpm --filter @composy/event-core run type-check
pnpm --filter @composy/event-core run build
pnpm --filter @composy/event-core run lint:check
pnpm --filter @composy/event-core run test:run

type-check 同时包含公开通配符载荷推导的 compile-time 断言。

构建产物

build 只通过 ldesign-builder 的内置 rolldown 打包器生成产物,并在构建后自动归一化和校验发布面:

Builder、ESLint 与 TypeScript 的权威配置位于本包 .ldesign/;根目录同名配置保留为兼容入口。

  • dist/index.js、dist/index.min.js 及 sourcemap:浏览器 UMD 入口;CDN 字段指向压缩入口。
  • es/:保留模块的 ESM 与 .d.ts,供 import、module、types 使用。
  • esm/:保留模块的 ESM 与 .d.ts。
  • lib/:保留模块的 CJS(.cjs)与 .d.ts,并包含 lib/package.json 的 CommonJS 边界。

pnpm run outputs:check 会检查 exports、main、module、types、browser、unpkg 和 jsdelivr 的每个目标均真实存在。