@xndrjs /形式
TypeScript面向意图的组合:declare 边界 和 意图,附加 供应商,用 compose。作曲拥有公众 表面惰性物化和执行上下文生命周期。
这个包是一个概念的证明。词汇存在于 GLOSSARY.md; 设计记录 文档/ adr / (边界的适配器对等体;统一 wiring 登记;Intent alias / Adapter passthrough默认;表面/using 关键卫生;设计C执行API;执行参数/ AbortSignal; 有效寿命推断;流意图模式/所有权 美国存托凭证0012; 入口边界和表面方向 美国存托凭证0013; 名义提供程序和运行时品牌 美国存托凭证0014; 边界误差累加- 美国存托凭证0015;可选 defineWiring 拔线时)。
安装
npm install @xndrjs/forma
快速启动
边界,意图,契约
import { boundary } from "@xndrjs/forma";
type OrderId = { readonly id: string };
type Order = { readonly id: string; readonly total: number };
type FindError = { readonly kind: "not_found" };
const Orders = boundary("orders");
export const FindOrder = Orders.intent("find").contract<OrderId, Order, FindError>();
export const GetOrder = Orders.intent("get").contract<OrderId, Order, FindError>();
那就先说名字吧 .contract<Input, Success, Err>() TypeScript会以这种方式保留字面意思的Intent名称。 boundary.intent 总是 一元 (一个终端 InvocationOutcome). 对于基于拉的序列使用 boundary.stream (下图)。每个边界意图退出都是累加的:实现抛出/拒绝变成 unexpected 在原点(美国存托凭证0015)。
供应商
.with 接受一个工厂,它返回Intent操作作为 function (同步或 async). 更喜欢 ctx => function (…) { … } 在嵌套箭头上。只有在使用上下文时才解构它。 Lifetime.onDispose 仅在物化尝试期间有效-在工厂返回后,手柄被密封;失败时,Forma立即回滚本地清理,并且不缓存该尝试。
生成的提供程序是一个不可变的标称描述符。它的公共元数据是 intent,
deps, origin, lifetimePolicy; 实现和参数推理机制仍然在内部。
使用 compose 执行提供程序行为——提供程序工厂不是公共调用或公共调用
猴子补丁缝。
import { expectedError, success } from "@xndrjs/forma";
const FindProvider = FindOrder.provide.with(
(_ctx) =>
function (id) {
return id.id === "42"
? success({ id: "42", total: 100 })
: expectedError({ kind: "not_found" as const });
}
);
type ConnHandle = { close(): void };
async function openConnection(opts: { signal: AbortSignal }): Promise<ConnHandle> {
opts.signal.throwIfAborted();
return { close() {} };
}
const Conn = Orders.intent("conn").contract<void, ConnHandle, never>();
const ConnProvider = Conn.provide.perContext().with(async ({ lifetime }) => {
const conn = await openConnection({ signal: lifetime.signal });
lifetime.onDispose(() => conn.close());
return function () {
return success(conn);
};
});
type OrderParams = { readonly role: "admin" | "viewer" | "guest" };
const GetOrderProvider = GetOrder.provide
.perContext<OrderParams>()
.using({ find: FindOrder })
.with(
({ deps: { find }, params }) =>
function (id) {
if (params.role === "guest") {
return expectedError({ kind: "not_found" as const });
}
return find(id); // forwards success / error / unexpected / cancelled
}
);
生存期策略是局部的,而有效生存期是从解析后的图中得到的。没有修饰语的意思 auto:提供商由组合根共享,除非任何传递依赖是上下文所有的,在这种情况下,它被缩小到每个实例一个 openExecutionContext() / withExecutionContext(). .perContext() 强制上下文所有权; .perComposition() pin根所有权和make compose() 如果传递依赖项需要上下文,则依赖项路径失败。 root.describePlan() 报告申报和有效期限。根与上下文 dispose() 是终端状态机(并发调用者共享一个承诺);进入 disposing 终止该所有者的 AbortSignal (lifetime.signal / execution.signal)。 root.dispose() 还关闭任何打开的执行上下文。更喜欢 withExecutionContext (或 await using 在打开句柄上)用于短词法范围;使用 openExecutionContext 当主机拥有整个挂载/卸载的生命周期时。
类型的执行 参数个数:声明一个普通的外部对象包上 .perContext<ParamsReq>(). compose({ … }) 推断 从所有此类声明中获取所需的宿主包(合并;键类型冲突导致组合失败)。打开/用然后要求 { params } (共享 ExecutionContextArgs). 在运行时,Forma只接受一个 Object.prototype/null-原型包与自己的可枚举字符串数据属性,克隆它,并浅冻结副本;嵌套的值不会被验证或冻结。执行句柄和所有逐上下文工厂接收相同的副本。工厂阅读打字 params 在 .with Context - not on Lifetime. compose<Explicit>()({ … }) 只是为了显式吗 超集 并应用相同的“尽力而为”静态形状检查。可选主机 signal 在这些参数上链接到上下文信号而不转移处理所有权。
Intent.provide.using 只接受相同边界的意图 只有一次的 (不要用链条 .using),并且需要字符串依赖键。表面别名同样是纯字符串的; dispose 和 surface 是否保留在合成表面上映射(操作下嵌套) execution.surface). 每个提供程序都有一个必需的 origin 判别: { kind: "boundary", id } 从 Intent.provide…with,或者 { kind: "adapter", id } 从适配器 .adapt…with.
适配器
一个 适配器 是Boundary的对等体,作用域为一个目标Boundary。它拥有目标意图和可能的外部实现 using 来自任何边界的意图。 默认为跨边界连接 -包括契约匹配-是一个适配器(或相同边界的提供者) 透传,而不是一个身份别名。这使原点、图边、生命周期和政策空间保持明确。
import { adapter, expectedError, success } from "@xndrjs/forma";
type PgOrderId = { readonly order_id: string };
type PgOrder = { readonly order_id: string; readonly amount_cents: number };
type PgFindError = { readonly code: "NO_ROW" };
const Postgres = boundary("postgres");
const SelectOrder = Postgres.intent("selectOrder").contract<PgOrderId, PgOrder, PgFindError>();
const OrdersAdapter = adapter("orders-postgres", Orders);
export const FindOrderViaPostgresProvider = OrdersAdapter.adapt(FindOrder)
.using({ select: SelectOrder })
.with(
({ deps: { select } }) =>
async function (id) {
const row = await select({ order_id: id.id });
switch (row.kind) {
case "success":
return success({
id: row.value.order_id,
total: row.value.amount_cents / 100,
});
case "error":
return expectedError({ kind: "not_found" as const });
case "unexpected":
case "cancelled":
return row; // pass through unchanged — never reinterpret as expected Err
}
}
);
.adapt 拒绝边界id与适配器的目标(编译时)不匹配的intent。结果是一个可私有注册的Provider wiring, origin: { kind: "adapter", id: "orders-postgres" }。适配器提供程序不能是表面根。地图只 success / error;通过 unexpected / cancelled 通过。不封装Forma依赖调用吗 try/catch 实现失败——依赖意图已经被累加。
当形状已经匹配时,工厂可以转发而不需要映射(return select(input)). 储备 aliasIntent(required, satisfier) 对于故意的同义词(rename / migration)。没有 forward 助手。
撰写和执行上下文
喜欢一个 内联 compose({ entryBoundaries, surface, wiring }) 文字。入口边界是准确的,非空的真实的集合 boundary() 值,其起源由Surface暴露。每个声明的入口边界必须拥有至少一个Surface别名,每个Surface根必须来自其中一个别名,适配器根将被拒绝。运行时标记验证协议的来源,而不是安全性。这使公共图的方向和提供程序的精度保持明确——你可以这样做 不 需要 defineWiring 在正常情况下。
import { compose, type InvocationOutcome } from "@xndrjs/forma";
const SelectOrderProvider = SelectOrder.provide.with(
(_ctx) =>
function (id) {
return id.order_id === "42"
? success({ order_id: "42", amount_cents: 10000 })
: expectedError({ code: "NO_ROW" as const });
}
);
const root = compose({
entryBoundaries: [Orders],
surface: { getOrder: GetOrderProvider },
wiring: [SelectOrderProvider, ConnProvider, FindOrderViaPostgresProvider],
// Optional: only automatic throw→unexpected conversions (once at origin).
onUnexpectedFailure(event) {
console.error(event.failure.id, event.cause, event.intent, event.phase);
},
});
const result: InvocationOutcome<Order, FindError> = await root.withExecutionContext(
async (surface) => {
return surface.getOrder({ id: "42" });
}
);
await root.dispose();
defineWiring / defineComposition 只有当你存在 提取 将(或整个选项包)连接到一个变量或模块中 const wiring = [A, B] 扩大到 Provider[] 并且进行类型检查,就好像缺少私有提供者一样。内联(或 as const / defineWiring)保持元组精确。不要将具体的配置注释为 ComposeOptions -扩大入口边界,表面和布线,并防止准确的来源/图形检查。拒绝在一个表面或布线槽内的提供商连接。
主机拥有的生命周期(没有回调),参数(从 GetOrderProvider的ParamsReq;使用 compose<Superset>() 只是扩大):
const rooted = compose({
entryBoundaries: [Orders],
surface: { getOrder: GetOrderProvider },
wiring: [SelectOrderProvider, ConnProvider, FindOrderViaPostgresProvider],
});
await using execution = rooted.openExecutionContext({ params: { role: "viewer" } });
await execution.surface.getOrder({ id: "42" });
中声明的表面别名 compose({ entryBoundaries, surface }) 是可调用的,嵌套的 execution.surface (生命周期停留在手柄上)。意图标识是结构化的对 (boundaryId, intentName) -同一对投掷在组合时间的竞争实现。该标识的重复声明必须在每次解析时具有完全相同的Input、Success/Item、Err和模态 using 优势;运行时还会在撰写期间拒绝具体化的模态不匹配。 compose 还会构建依赖计划(using + Intent-alias边),拒绝全路径诊断的循环,需要一个封闭图,并在返回根之前导出有效生存期。 describePlan() 包括入口边界id、表面根来源、边界/适配器来源和嵌套 lifetime 每个提供商的解释。私人供应商和 aliasIntent() 条目共用一个 wiring 数组;跨界布线首选适配器直通。看到 apps/forma-demo 有关双适配器端到端示例(postgres | memory,参数策略; WatchOrders 委托给私有的入口流 WatchOrderEvents 接缝,异步连接,推断生命周期, await using 根)。
流的意图
声明一个 流 合同 boundary.stream(name).contract<Input, Item, Err>()。相同 .provide / .adapt 建造者;工厂返回一个 StreamOperation (async function* 或任何 AsyncIterable 生产者)接收 (input, invocation) 哪里 invocation.signal 每个调用都是,并且从属于执行上下文信号。
type OrderEvent = { readonly orderId: string; readonly status: string };
type WatchError = { readonly kind: "unavailable" };
const WatchOrders = Orders.stream("watchOrders").contract<OrderId, OrderEvent, WatchError>();
const WatchOrdersProvider = WatchOrders.provide.with(
() =>
async function* (id, invocation) {
try {
invocation.signal.throwIfAborted();
yield success({ orderId: id.id, status: "open" });
yield success({ orderId: id.id, status: "paid" });
} finally {
// runs on natural end, break, return(), AsyncDisposable, or context/root dispose
}
}
);
const streamingRoot = compose({
entryBoundaries: [Orders],
surface: { watchOrders: WatchOrdersProvider },
});
await using execution = streamingRoot.openExecutionContext();
for await (const event of execution.surface.watchOrders({ id: "42" })) {
if (event.kind !== "success") break; // error / unexpected / cancelled are terminal
// …
}
所有权和取消(见 streaming-intents.md, 美国存托凭证0012):
- 每个表面调用返回a 冷,single-consumer
AsyncIterable(+[Symbol.asyncDispose]). 创建它并不能实现提供者;第一个next()所做的。 - 每个活动流调用都是 由执行上下文拥有 (lease + registry),独立于提供商的有效生命周期——包括来自组合有效提供商的流。
- 第一个
next()在调度源之前以事务方式获取注册中心/租赁所有权。如果接受失败,则不会启动源,并且通过正常的关闭/错误矩阵回滚部分所有权。 break/return()/await using在可迭代对象上,contextdispose,根dispose关闭活动流(中止每个流信号→)return()→等待finally→释放一下)。取消一个流不会中止兄弟流或上下文。- 使用已中止的主机信号打开立即失败。稍后的宿主中止拒绝新的一元工作
signal.reason,一股尚未开始的冷流在没有物化的情况下完成。已接受的工作配合观察取消。宿主中止 不 汽车- - - - - -dispose()上下文(abort≠dispose)。 - 流清理失败由发起close的调用方观察到。上下文/根处理仍然尝试每个流和提供程序清理,并聚合多个失败。
- 适配器保留模态;一元流↔流Intent别名在编译时失败。
using流意图解析为StreamOperation,绝不是开放迭代器。describePlan()报告mode: "unary" | "stream"在每个已注册意图上。
验证发布的包
从存储库根目录运行 pnpm validate:forma-package。该命令构建Forma,创建发布到npm的相同的tarball形状,用 publint 和类型是错误的,将其安装到一个孤立的消费者中,并对该消费者进行类型检查 skipLibCheck: false,并运行一元/流/处置烟雾测试。它永远不会解决 @xndrjs/forma 通过工作区源别名。
状态
PoC (0.1.0-alpha.0):封闭的作文; .contract边界的适配器对等体,罕见的结构 aliasIntent,图形导出有效寿命,统一 .with({ deps?, lifetime })设计C执行API,异步工厂,流意图模式,边界错误累加(InvocationOutcome),处理。不是通用的IoC容器。