Node 模块与 Worker 任务
为独立 Node ESM 和 CPU 密集任务选择合适的构建与生命周期模型。
Pluxel 可以把一段代码构建成独立 Node ESM,也可以把 CPU 密集任务放进共享线程池。两者解决的问题不同:
| 需求 | 选择 |
|---|---|
| 把另一份源码构建成独立 Node ESM,并在当前线程初始化 | defineNodeModule() |
| 把可结构化克隆的 CPU 密集工作放入共享线程池 | defineWorkerTask() |
| 普通异步 I/O、数据库或短小调用 | 直接在 Plugin 中执行 |
在 快速开始 创建的插件包内添加下列文件;保留 Pluxel 的 Vite/构建配置,普通 TypeScript 编译不会生成这些独立产物。声明必须位于模块顶层,入口使用静态相对路径。
独立 Node module
import { , , } from '@pluxel/runtime'
const = (import.meta., './rules-entry.ts')
@({ : 'Rules' })
export class extends {
protected override async () {
await this...(, async () => {
const = await import(.)
return .setup({ : this.. })
})
}
}rules-entry.ts 默认没有 Pluxel Context;它导出上面调用的 setup(),返回释放资源的函数:
export function setup({ logger }: { logger: { info(message: string): void } }) {
logger.info('rules module ready')
return () => logger.info('rules module released')
}启动插件后应看到 rules module ready;停用插件后应看到释放日志。
defineNodeModule() 只声明 entry,不创建线程。ctx.nodeModules.use():
- 等待 artifact 首次可用和 setup 完成;
- setup/import 失败会让 Plugin
init()失败; - callback 返回的 cleanup/disposable 绑定当前 owner generation;
- 开发期 replacement 先 setup 新 URL,成功后再清理上一成功消费者;
- 新 artifact 失败时保留旧成功 consumer,不提交半成品。
artifact 是自包含单文件 Node ESM。它可以使用 Node builtin 和可安全 bundle 的普通 library,但不能 value-import Pluxel runtime/core、Plugin、Context 或 Workbench server API,也不能嵌套声明 Plugin/Workbench/Node module。
需要把 Context 能力交给 module 时,传入窄的、明确 owner 的 facade,不要传整个 Context 或 Plugin instance。
共享 worker task
持续占用 JavaScript event loop、thread-safe 且能用纯数据描述的 CPU/native 工作使用 worker task:
import { , , } from '@pluxel/runtime'
type = { : number[] }
type = { : number }
const = <, >(import.meta., './sum-worker.ts')
@({ : 'Reports' })
export class extends {
(: number[], ?: AbortSignal) {
return this...(, { }, { })
}
}把下一段保存为与插件文件同目录的 sum-worker.ts。它默认导出 handler,不接收 Pluxel Context:
import type { } from '@pluxel/runtime'
type = { : number[] }
type = { : number }
const : <, > = ({ }) => ({
: .((, ) => + , 0),
})
export default 调用 calculate([1, 2, 3]) 应得到 { total: 6 }。用 测试宿主 或现有应用的 开发控制台 调用插件方法,才能同时验证产物提取与执行路径。
所有插件共享宿主的线程池,线程按需创建,队列有上限,并在插件之间公平调度。host 统一配置 concurrent execution slots、全局和每 Plugin queue limit、idle timeout;Plugin 不创建私有线程池,也不自行扩大进程预算。取消 running task 会终止对应 worker; caller Promise 可以立即结束,但 runtime 会等 worker 真正退出后才归还 active slot,避免 replacement task 穿透执行上限。
什么可以跨线程
输入输出必须满足 structured clone:
- 可以:plain object、array、string、number、boolean、
ArrayBuffer、typed array; - 不可以:function、closure、Context、Plugin instance、数据库连接、native Canvas/Image;
- native binding 必须明确 thread-safe,并由拥有它的 package 声明构建 metadata。
网络/数据库 I/O、短调用、不可重试 side effect 通常不应为了“统一”而进入 worker。worker cancellation 可能终止线程,任务必须能安全重做或丢弃。
大型二进制 transfer
默认输入在 run() 接纳时 snapshot。大型 ArrayBuffer 可以明确移交 ownership:
const bytes = new Uint8Array(await response.arrayBuffer())
const result = this.ctx.workers.run(
decodeTask,
{ bytes },
{
signal,
transfer: [bytes.buffer],
},
)
// 接纳成功后 bytes.buffer 已同步 detached。
return resulttransfer 规则:
- 只接受
ArrayBuffer; - 同一个 buffer 不能重复列出;
- accepted 后即使任务最终失败也不会恢复 ownership;
- queue 已满、任务未接纳时 runtime 不 detach;
SharedArrayBuffer本来就是共享内存,不进入 transfer,调用方自行负责同步协议。
借用输入,避免排队时复制
默认 snapshot 适合调用后立即复用或修改 input。若领域 API 已经要求 input 在 Promise settle 前保持不变,可以省略 排队 snapshot,只保留真正 dispatch 的 transport clone:
return this.ctx.workers.run(renderTask, input, {
signal,
inputOwnership: 'borrowed',
})borrowed 模式在 queue 中保留 caller graph,因此 mutation 会改变尚未 dispatch 的任务;它不能与 transfer 同时使用。
该模式减少一次 clone,不会让 postMessage serialization 离开主线程,领域仍必须对超大 object graph 设置 bytes/count/depth
预算。
任务获准执行后再准备输入
如果 domain budget walk 或 snapshot 本身较重,先用共享 queue admission,再准备输入:
return this.ctx.workers.runPrepared(
renderTask,
async (signal) => {
await assertBoundedDeclarativeGraph(option, signal)
return { option, policy }
},
{ signal, inputOwnership: 'borrowed' },
)prepare(signal) 只在 artifact route 可用、任务轮到 fair execution slot 后执行;queue full 不调用它。准备期间该 slot
保持 active,因此 host-side preflight 数也受 maxThreads 限制。callback 应分段让出 event loop 并观察 signal;runtime
不能抢占一个同步 callback。callback error 原样返回,prepared value 的 structured-clone error 仍归类为
WorkerTaskError('INVALID_INPUT')。
默认会 snapshot prepared value;只有领域 API 已要求 caller 到最终 Promise settle 前保持 captured/returned graph 不变时
才使用 borrowed。runPrepared() 不支持 transfer,因为 admission 时 buffer 尚未产生,无法提供“接纳成功即同步 detach”
的 ownership contract。
取消与 Plugin stop
run(task, input, { signal }) 的 signal 取消 admission 或运行中任务。Plugin stop/replacement 也会取消并等待该 owner 已接纳任务,不让旧 generation 在后台继续写结果。
不要只 fire-and-forget:
// 错误:调用方无法观察失败,业务也不知道结果是否提交。
void this.ctx.workers.run(task, input)后台调用需要明确捕获 error、更新有界状态,并决定重试/丢弃。请求级任务把 Promise 返回给 HTTP/command 边界。
Canvas 与 ECharts
native Canvas/Image 不能 structured clone。官方 Canvas Plugin 提供纯数据 workerSnapshot 和 @pluxel/canvas/worker adapter,让 worker 在自己的线程内创建 native surface;业务插件不直接传 native handle。
@pluxel/echarts 始终使用 runtime shared worker pool。调用方只需调用 render(),不要再套一层自建 worker。
构建与验证
Plugin build 会提取 literal declaration,并输出独立 artifact。验证至少覆盖:
- declaration 使用 literal relative entry;
- artifact 中没有 Pluxel runtime/Context import;
- input/output 可 clone,transfer 后调用方不再访问 buffer;
- queue full、abort、worker error 和 Plugin stop;
- native dependency metadata 与真正 package owner 一致。
构建产物与缓存位置见 CLI 与工具链,测试 host 选择见 测试 Pluxel 插件。
最后更新于