插件开发范式

事件:广播与依赖协议

使用 Core 自带事件传递通知,区分 ambient 广播、provider channel 与异步完成语义。

ctx.events 和 EvtChannel 属于 Core,不需要安装服务。先判断是否真的需要通知:需要一个指定提供方执行操作并返回结果时,直接调用依赖方法。

需求选择保证
没有特定 provider 的宿主内通知ctx.events当前 root 内广播,订阅归调用方 owner
某个 provider 的公开事件,consumer 必须依赖它provider 上具名 EvtChannel依赖关系与订阅归属都可追踪
请求、查询或需要一个明确业务结果constructor dependency + 方法返回值与失败由领域契约定义

同一 Host 的广播

需要在同一宿主中广播消息,而且不要求某个插件存在或先启动时,使用 ctx.events。这种无特定提供方的广播称为 ambient 事件。先扩展 Events 类型,再通过 ctx.events 订阅和发布:

import { ,  } from '@pluxel/core'

declare module '@pluxel/core' {
	interface Events {
		'catalog:invalidated': [catalogId: string]
	}
}

@({ : 'Catalog observer' })
export class  extends  {
	protected override () {
		this...('catalog:invalidated', () => {
			this...('catalog invalidated', {  })
		})
	}
}

发布端在自己的业务方法中调用:

this.ctx.events.emit('catalog:invalidated', catalogId)

事件类型应放在双方能导入的领域契约模块中;保证 augmentation 进入各自 TypeScript 编译范围,不通过导入一个业务插件来偷偷安装依赖。

module augmentation 只合并 TypeScript 事件词汇,不会 import、安装、打开 auto-start policy 或连接两个 Plugin,也不提供启动顺序保证。每个 root 共享一个 emitter backend,但每个 Plugin、Part 和 caller Context 得到固定 owner 的普通 EventsService view;订阅自动进入 该 owner effects,stop、replacement 和 init rollback 都会取消订阅。事件名应使用带领域前缀的稳定字面量,避免无归属的通用名称。

Provider 的公开事件

如果事件属于某个 provider 的公开能力,consumer 必须依赖该 provider,或者 availability 会影响 consumer lifecycle,则公开 具名 EvtChannel,不要把依赖伪装成 ambient 广播:

import { , ,  } from '@pluxel/core'

type  = { : string }
type  = (: , : ) => void | <void>

@({ : 'Billing' })
export class  extends  {
	readonly  = {
		: new <>(this.),
	} as 
}

declare function (: , : { :  }): <void>
@({ : 'Invoice projection' })
export class  extends  {
	constructor(private readonly : ) {
		super()
	}

	protected override () {
		this....(async (, ) => {
			await (, {  })
		})
	}
}

listener registration 会绑定调用方 Context effects,stop/replacement 时自动取消;仍可以使用返回的 disposer 提前移除。生产者需要等待所有异步 listener 并隔离单项失败时,使用 emitSettled()。 EvtChannel 直接接收 this.ctx;不要传 () => this.ctx。调用方归属由 dependency caller facade 显式绑定,不通过动态 Context provider 推断。

发布完成与异步失败

ctx.events 和 EvtChannel 都提供以下发布形式:

方法适合什么调用者必须知道
emit(...)同步通知或不等待异步处理的广播返回通知计数,不是业务成功数,不等待 Promise 完成
await emitAll(...)需要所有结果、任一失败即报告失败reject 不代表已开始的其他工作被取消或回滚
await emitSettled(...)等待本次各 listener 的成功/失败记录必须消费每项 status;不会自动重试失败 listener

下面是 BillingPlugin 内部方法中的发布片段,signal 由本次业务操作传入。它是公开事件参数,不会由 channel 自动补上:

const outcomes = await this.events.invoicePaid.emitSettled(invoice, signal)
for (const outcome of outcomes) {
	if (outcome.status === 'rejected') {
		this.ctx.logger.error('invoice projection failed', {
			invoiceId: invoice.id,
			error: outcome.reason,
		})
	}
}

订阅与等待的寿命

  • 在 init() 或明确的操作作用域中订阅。on() 返回 disposer,可提前撤销;普通订阅随 owner 停止、替换或初始化回滚自动移除。
  • 只等下一次通知可用 once();waitFor() 返回可取消的 Promise,必须等待/处理拒绝,并按操作需要设置 signal 或超时。owner 清理会取消等待。
  • 取消订阅只阻止后续通知,不等于正在执行的 listener 已停止。长任务使用明确的 signal 和资源清理,不要在 handler 中留下无监督的 Promise。
  • 广播不是持久队列,也不补发订阅前的历史通知。需要恢复状态时先查询权威状态;需要必需的启动顺序时声明 dependency,不等待一条可能错过的“ready”广播。

验证 consumer 能收到事件、提前撤销和停止后不再收到,以及异步 listener 失败能够被调用者观察。不要只测试类型 augmentation 能编译。

最后更新于

本页目录