@composy/event-core
v0.0.2
Published
LDesign 事件系统 - 框架无关的核心模块
Maintainers
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:runtype-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 的每个目标均真实存在。
