@ frondruntime/ core (中文(简体) )
Core Frond 运行时包:节点作者,图执行,生命周期,诊断,信号,和测试用带.
安装
bun add @frondruntime/core effect mobx
effect (stable 4.x)是Effect-facing驱动器和运行时间API的对等依赖. mobx 是一个对等依赖, 因为 Frond 节点状态是 MobX 备份, 必须共享 app 的 MobX 运行时间 .
它拥有什么
createRuntime和运行时间客户端.NodeBase, (中文(简体) ),NodeSpec,有口味的斯克工厂(nodeSpec.async编号 :.effect,对serviceSpec, (中文(简体) ),resourceSpec, (中文(简体) ),facadeSpec),标记,密钥,依赖性,结果承诺.Driver.Async财务报告和审定财务报表Driver.Effect用于预建或共享驱动器的构建器(用于.fromDriver财务报告和审定财务报表specWithDriver); 内线作者通过口味工厂进行.- 图/运行时间类型
Frond.Graph, (中文(简体) ),Frond.Runtime, (中文(简体) ),Frond.Events, (中文(简体) ),Frond.Signals,以及Frond.Diagnostics。 。 。 . - 会话流程助手 :
readReady编号 :ensureReadyNode单飞行空投行动admission: "join", (中文(简体) ),runTransition编号 :createTransition用于命令的多节点序列,以及pendingOperations编号 :isQuiescent运行时间读取. - 信号总线:
Signals.defineChannel(可选择由事件映射输入),runtime.publish, (中文(简体) ),runtime.subscribeSignals,以及ctx.signals司机内部. - MobX- Facing 节点辅助工具
Frond.MobX。 。 。 . - 外部托管公用事业:
withInternal编号 :internalOf编号 :carryInternal隐藏必要的内部结果封套,以及createRuntimeCoordinator用于序列化运行时间替换(dev HMR,测试隔离). - 测试助手
@frondruntime/core/testing。 。 。 .
最小节点
import * as Frond from "@frondruntime/core";
type SessionSpec = Frond.NodeSpec<{
readonly mode: "async";
readonly args: Frond.Args.None;
readonly key: Frond.Key.Singleton;
readonly result: { readonly userId: string | null };
}>;
export class SessionNode extends Frond.NodeBase<SessionSpec> {
static readonly spec = Frond.serviceSpec.async<SessionSpec>({
tag: Frond.tag("app/session"),
key: () => Frond.Key.singleton(),
acquire: Frond.Driver.Acquire(async () => ({ userId: null })),
});
get isSignedIn(): boolean {
return this.result.userId !== null;
}
}
驱动模式声明一次, 以 spec 形状(readonly mode: "async" | "effect") (中文(简体) ). 工厂的口味必须同意 .async 需求 mode: "async", (中文(简体) ), .effect 需求 mode: "effect" - 还有 NodeBase<Spec> 从形状中衍生出节点的动作表面,所以三个绝对不能有分歧.
驱动钩接收到两个中止信号. ctx.signal 已设定操作范围:当当前获取/更新/操作中断或中断时中止. ctx.nodeSignal 是节点-寿命时间:每个已准备好的节点化身1个,在获得跑道之前创建,并在活道关闭(放行,放行,准备失效,跑道停止)时完全中止一次. 在获取中注册的固定长寿回调 - 订阅,外部收听器 - 到 nodeSignal; 使用 signal 取消行动本身的飞行工作.
运行时间使用
const runtime = Frond.createRuntime();
const session = runtime.client.node(SessionNode, Frond.Args.none);
await session.ensureReady();
const read = session.read();
仅可读出作者的节点实例 。 待发,出错,无效,不可用,且未接通的读取会显示生命周期数据,而非部分节点对象. A 类 Ready 已完全输入 : read.result 完全就是宣布的结果类型( 绝不是 | undefined),以及 read.node 是编译的类实例 - 没有铸件 .
当一个呼叫站点想要打出一个已打入的节点或一个被扔出的错误而不是一个被标记的读取时,手柄会提供一个即时或掷出投影:
// Sync: Ready returns the typed node instance, Error rethrows the read's
// underlying error, and unwired/idle/pending throw the typed
// Runtime.FrondNodeNotReady (carries nodeId, tag, readiness).
const node = session.readReady();
// One awaited readiness attempt (ensureReady) followed by the same projection.
const ready = await session.ensureReadyNode();
readReady 从不安排图表工作; ensureReadyNode 替换手卷的已读或已掷入的包装和它们的铃声错误类.
在 React 适配器外订阅时, 手柄对 subscribe 与 readVersion数字 : session.readVersion() 返回节点承诺状态的修订,在无变化时稳定在呼叫之间,因此它充当 getSnapshot 半个 useSyncExternalStore- 风格融合而不是散列 read() 手边。 修改在一连串驱逐中保持单调状态:一个被重新创建的节点继续过其前身的修订,因此之前观察到的版本都无法再出现.
会话流程
无效输入诉讼可以宣布加入 Frond.Driver.Action(run, { admission: "join" }) - 因此,同时在常数的每个节点/行动键上引用单飞行,并且都分享一个飞行中的结果:
expire: Frond.Driver.Action(async () => performSignOut(), { admission: "join" }),
仍然需要投入行动 admissionKey(input); 无效输入加入拒绝一个明示 admissionKey 在编译时间.
有序的多节点序列 - 退出,会话过期 - 通过 runTransition编号 :createTransition数字 :
const signOut = Frond.createTransition(
[
{ label: "expire-session", run: () => session.actions.expire() },
{ label: "clear-caches", run: () => clearCaches() },
],
{ onStepFailure: "continue" }
);
const outcome = await signOut(); // concurrent calls join the in-flight run
if (!outcome.ok) {
report(outcome.failures); // each failed step's label and cause
}
步骤严格按部就班地运行,并可能返回承诺或自成一体的效果。 一项失败政策适用于整个运行: "abort" 在第一次失败时停下来 然后跳过剩下的, "continue" 运行每个步骤并收集每个失败(最优的尾巴). 回归的诺言永不拒绝 TransitionOutcome 记录报告 completed, (中文(简体) ), failures,以及 ok故意尽量减少:没有每步暂停、补偿或可恢复性.
对于观察, runtime.pendingOperations() 列出当前操作的节点 Running (单位:千美元){ nodeId, tag, operation }),以及 runtime.isQuiescent() 与布尔读取的瞬间相同。 两者都是数据预测 getSnapshotSync() 已经携带 - 可观察性读取,而不是等待-静默屏障:操作可以在读取和任何代码之间启动或解决.
信号
信号是一款在流程中,最优等的信息总线,在图外刻意. 应用程序的多个部分所关心的域事件 - checkout.completed, (中文(简体) ), session.expired - 旅行时没有成为别人的节点结果或连环动作呼叫,而devtools将它视为自己的事件类别,而不是通用的跑时噪音.
一个频道宣布它携带什么作为类型地图,以及 defineChannel 键入它的频道 :
interface CheckoutEvents {
"checkout.started": { readonly cartId: string; readonly total: number };
"checkout.completed": { readonly orderId: string };
"app.opened": void; // an event that carries nothing
}
export const Checkout = Frond.Signals.defineChannel<CheckoutEvents>({
name: "app.checkout",
policy: { retention: "bounded", bufferSize: 256 },
});
const runtime = Frond.createRuntime({ channels: [Checkout] });
Checkout.signal 仅接受已申报的名称,每个名称连同有效载荷其地图条目申报。 输入的事件 void 完全不使用有效载荷参数,而且地图中不包含的名称是一个编译错误,而不是无人订阅的信息:
await runtime.publish(Checkout.signal("checkout.started", { cartId, total }));
await runtime.publish(Checkout.signal("app.opened"));
地图只是类型 。 没有在发布时解码:信号有效载荷由消耗它的同一种应用程序所构建,所以运行时检查会花费工作来重新发现编译器已经拒绝编译的错误. 显示类型参数保留未键入的信道 - 任何名称, 一个 unknown 有效载荷 - 因此现有信道无需更改,信道可以一次打入一个.
从节点上订阅
Checkout.subscriber 建立固定在自己频道上的订阅者,并将每个分支缩小的记录交给处理者。 登记在册 acquire {\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}然后撕下来 ctx.disposers这是整个分析节点的食谱:
acquire: Frond.Driver.Acquire((ctx) =>
Effect.gen(function* () {
const store = observable({ orders: 0 }); // the node's result; observable so React re-renders
const subscription = yield* ctx.signals.subscribe(
Checkout.subscriber({
name: "analytics",
handle: (record) =>
Effect.gen(function* () {
switch (record.signal.name) {
case "checkout.completed": {
// Narrowed to this branch's payload: orderId, no cast.
track(record.signal.payload.orderId);
yield* ctx.deps.orders.actions.counted({ by: 1 });
break;
}
case "checkout.started":
case "app.opened": {
break;
}
}
}),
})
);
ctx.disposers.add(subscription.unsubscribe);
return store;
})
),
食谱所依赖的两件事. ctx.disposers 将订阅者绑入节点化身,因此在释放,驱逐时被撕毁,或者运行时被停止,而不会活出拥有节点的节点. 一个需要达到图态的操作员 通过对一个 宣布的依赖关系 - 说吧 ctx.deps.orders 上面——这是所有其他外图回调的相同学科如下: 目标细胞演员将动作序列化,因此一阵突起的信号无法在更新时半途相接.
每个处的收缩是投放滤波器的投影,而不是声称: subscriber 针头 channels 到这个频道,公交车过滤 发送完全通过那个字段。 用户手动组装,不用 channels 接收每个频道的流量 并正确得到宽度 RuntimeSignalRecord对于图外的消费者来说, runtime.subscribeSignals(subscriber) 带同值并返回同值 unsubscribe。 。 。 .
交付费用
publish 等待它的订阅者,一个一个地等待它解决。 这就是让一个解决的出版商“每个订阅商都运行了”的含义,这也是成本:一个缓慢的处理者会拖延出版商和每一个订阅商的背后。 保持处理器短而叉出什么不是(Effect.forkDetach,或者排队 节点排出它自己)。 处理器失败时不失败 - 失败时报告为 RuntimeSignalSubscriberFailureObserved 事件同时命名用户和信号,其余的继续发送.
调度器节点
一个信道常数是一个模块单通,因此它不能持有每个会话中任何有差异的. Analytics希望完全相反:一个会话id,当前屏幕,以及每个事件的签名用户. 这个信封是模块常数无处可藏的部分 连接到每个呼叫站点 而不是它如何僵化 固定方式是通过将信封放入节点后,将信封放入节点的化身中并随同放出,来将发件捆绑到运行时间.
发送方法在节点上 结果中,没有 actions。一个动作通过节点的细胞演员进行路由,它序列化了——一个状态更新的正确保证,错误的在这里,因为通过一个演员重新引入阻塞总线的线头来引导应用程序中的每一个调度。 行动还发出一个承诺或每个呼叫站点必须放弃的效果。 对结果的一种方法是 完全关闭 ctx.signals.publish:没有演员可以进入,没有等待.
分析调度是火和遗忘,所以方法返回 void。为此使用一个同步-模式节点 - 在那里 ctx.signals.publish 返回时 Promise<void> 而这个方法可以放弃承诺,在实际模式中它必须运行效果本身.
放弃它意味着捕捉它,而不是消灭它。 出版被认可为运行时间工作,所以拒绝 FrondRuntimeClosed 一旦运行时间停止, 和发送赛跑 撕裂正是发生的时候。 一个空的捕捉器就在这里, 因为它的方法是火和遗忘: 呼叫者从来没有被告知是否交付, 分析器是最后一个子系统.
import * as Frond from "@frondruntime/core";
// Exported: features extend it by declaration merging (below).
export interface AppEvents {
"app.opened": void;
"cta.clicked": { readonly cta: string };
}
// NOT exported. The only way to build a signal on this channel is `track`.
const Analytics = Frond.Signals.defineChannel<AppEvents>({
name: "app.analytics",
policy: { retention: "bounded", bufferSize: 256 },
});
interface Envelope {
sessionId: string;
screen: string;
userId: string | null;
}
class Dispatcher {
constructor(
private readonly publish: (signal: Frond.Signals.RuntimeSignal) => Promise<void>,
private readonly envelope: Envelope
) {}
track = <K extends keyof AppEvents & string>(
name: K,
...args: Frond.Signals.SignalPayloadArgs<AppEvents[K]>
): void => {
const [payload, metadata] = args;
this.publish(
Frond.Signals.signal({
channel: Analytics.channel,
name,
payload,
metadata: { ...this.envelope, ...metadata },
})
).catch(() => {
// Publishing to a stopped runtime rejects. Analytics is the last thing
// that should take an app down on its way out.
});
};
screen(name: string): void {
this.envelope.screen = name;
}
}
type DispatcherSpec = Frond.NodeSpec<{
readonly mode: "async";
readonly args: Frond.Args.None;
readonly key: Frond.Key.Singleton;
readonly result: Dispatcher;
}>;
export class AnalyticsDispatcherNode extends Frond.NodeBase<DispatcherSpec> {
static readonly spec = Frond.serviceSpec.async<DispatcherSpec>({
tag: Frond.tag("app/analytics-dispatcher"),
key: () => Frond.Key.singleton(),
acquire: Frond.Driver.Acquire(
async (ctx) =>
new Dispatcher((signal) => ctx.signals.publish(signal), {
sessionId: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
screen: "boot",
userId: null,
})
),
});
}
会话 id 是从 Date.now 财务报告和审定财务报表 Math.random 而不是说 crypto.randomUUID,出于同样的原因,devtools客户端停止称它: React Industrial没有全球 crypto 直到一个多填充器安装一个,一个会话 id 需要是一个进程内独有的,而不是不可理解的.
通用保存了频道的每个名字的完整打字: track("cta.clicked", { cta: "buy" }) 被精确检查为 Analytics.signal 将检查它。 一个未申报的名字,一个错误的有效载荷,一个有效载荷传递给一个被宣布的事件 void,而省略的所需有效载荷则通过此路径分别是一个编译错误.
内部不需要铸造,因为 Signals.signal({ channel, name, payload, metadata }) 声明 payload: unknown。另一种拼写不起作用,值得命名,所以没有人会重新发现它: 分解结构 ...args 将呼叫重新组合为 Analytics.signal(name, payload, metadata) tS2345 失败, 因为有条件的Tuple SignalPayloadArgs<AppEvents[K]> 无法从自身结构化元素重建,同时 K 仍然是通用的。 对象形式完全绕过重建 .
这种形状能买到两样东西,都用量度。 发送时间不等待交付:3个发送点在1ms中返回,一个用户每起事件服用100ms. 信封是活的,而不是每个电话的捕捉: screen: "boot", (中文(简体) ), screen("cart") 然后被召唤, 接下来的二人带着 screen: "cart" - 用户和保留缓冲区.
成本是冷的开始,是诚实的: useNode 暂停,因此一个组件在获取调度器节点之前无法发送,而最有可能丢失的正是冷启动的事件. Preload 在应用根的调度器, 高于任何燃烧的:
<FrondReact.Preload nodes={[{ analytics: [AnalyticsDispatcherNode, {}] }]}>
<Routes />
</FrondReact.Preload>;
// Anywhere below it, with no suspense left to pay:
const analytics = FrondReact.useNode(AnalyticsDispatcherNode, {}).result;
<button type="button" onClick={() => analytics.track("cta.clicked", { cta: "buy" })}>
Buy
</button>;
这不是重新投降的风险. _result 实值 observable.ref 信封在马厩里变了 Dispatcher 实例,所以当信封改变时,只发送组件不会重递。 翻转的一面是从中遵循的规则:不要从信封中渲染出,因为没有什么会告诉React它移动了.
"一种发送方式"作为模块边界执行,而不是作为类型. 频道常数不导出, 所以没有其它模块可以调用 Analytics.signal ;事件映射界面会保留导出,因为声明合并需要它,所以特性会不断贡献事件而不获得发布的方式. 其他节点通过宣布的依赖达到相同的方法 - ctx.deps.analytics.result.track(...) - 这是普通的依赖路径,而不是第二道门.
这个合并的地图使得一个全应用的频道成为可行. 一个功能模块扩展了它自己的事件所在的共享界面:
// features/cart/events.ts
declare module "../../analytics/dispatcher" {
interface AppEvents {
"cart.item.added": { readonly sku: string; readonly quantity: number };
}
}
信道常数是在加载这些扩能器之前创建的,并且仍然能看到每个扩能器,因为合并是一种类型级操作 defineChannel 携带的不是每起事件 运行的文物。 安全网在订户一方:一名操作员,他 default: 分支这样做 const unhandled: never = record.signal 停止编译任何特性添加事件的瞬间,所以共享的映射无法悄悄地从处理它的代码中成长.
保留也是隐私的杠杆
一个频道的 policy 决定保存什么. bounded 保留最后一个 bufferSize 记录(省略后为128个),可读取 ctx.signals.readRetained 且能见于饲料中. none 保留一无所有 - 和条 payload 不仅从缓冲器,而且从排放的运行时间事件和水槽交付。 因此,一个频道 携带任何不应该离开程序 retention: "none":其有效载荷没有接通电线,而其信道和名称仍然如此,这足以使其看到发射.
什麽 bounded 保留为有效载荷 参考文献:记录被推为已发布,没有克隆和冻结. 所以,一个你不断变异的有效载荷 出现在任何它已经到达的状态 在读到种子时, 而不是它在发布状态 bufferSize 该频道的出版物。 而不是把应用程序编辑的对象交给总线.
不依赖图表
信号不参与准备。 没有东西等待一个,交付是最佳的,一个在发射信号后获得的节点不会看到重播,除非它要求重播一个(ctx.signals.readRetained),因此从交付中得出的节点结果会因靴子订单而异. 依附关系通过 dependencies编号 :dep;一个必须改变图表状态的信号通过援引一个动作来完成,如上.
间接
节点动作是模式内在的:一个效果-节点的动作返回 Effect,一个aync-mode节点的动作返回 Promise。 。 。 . unwrapEffect 财务报告和审定财务报表 wrapPromise 越过边界,向两个方向.
// Call an effect-mode action from Promise/React code.
await Frond.unwrapEffect(session.actions.refreshToken({ force: true }));
// Call an async-mode action (or any outside Promise) from an Effect pipeline.
yield* Frond.wrapPromise(() => profile.actions.rename({ name }));
测试
import * as FrondTest from "@frondruntime/core/testing";
测试子路径包括运行时的吊带,被推迟的驱动器,以及用于测试节点行为而无需反应的 spec helpers.
移动
从0.4.0升级? 错误报告改变形状;见 移民-0.5.md。 。 。 .
从0.1.0升级? 包含片段前后的完整断开更改列表在 移民-0.2.md。 。 。 .
文档
- 安装 : https://frondruntime.dev/docs/start/install 页面存档备份,存于互联网档案馆
- 第一个节点: https://frondruntime.dev/docs/start/ 第1个节点
- 公众表面 : https://frondruntime.dev/docs/reference/public-地表地图
此软件包的放出注释来自常规协议 . Squash-commerd PR标题提供了本套件的放出注释主题. 释放烟雾测试在npm发布前验证了GitHub发布元数据.
大赦国际的使用
Frond是AI辅助(主要是Claude和Codex),在数月内被反复使用,而不是一发生成. 全注: https://frondruntime.dev/ai-use (英语)