@beignet/网络
运行时间 : Beignet需要Node.js 22.12或更新. Bun是可选的.
[叹息] Beignet是实验性的α软件. 这个
0.0.x包行为早期 评估,且在框架结算时,API可能在发布之间发生变化.
Beignet 框架运行时间的 Web 获取适配器 .
运行时间接受标准时使用此软件包 Request 返回 a
标准 Response例如Cloudflare Works, Bun, Deno, Node 获取服务器,
或应避免特定框架适配器的测试.
安装
npm install @beignet/web @beignet/core
代理技能
这个包装的是谭斯塔克特意技能
@beignet/web#fetch-server。在编译 Web 获取运行时间时装入它,
配置 CLI 网络配置文件, 安装 Bun 或无服务器处理器, 返回
原生反应、添加生道或测试 @beignet/web/testing。 。 。 .
快开始
import { createFetchServer } from "@beignet/web";
import { routes } from "./server/routes";
export const server = await createFetchServer({
ports,
routes,
context: ({ ports, requestId }) => ({
requestId,
ports,
}),
mapUnhandledError: () => ({
status: 500,
body: {
code: "INTERNAL_SERVER_ERROR",
message: "Internal server error",
},
}),
});
export default {
fetch: server.fetch,
};
阿宝
import { server } from "./server";
Bun.serve({
fetch: server.fetch,
});
服务工作
声明 context.service 当队列、时间表、任务、种子或其他
非 HTTP 工作需要与请求相同的组装应用程序上下文 。 使用
server.runServiceContext(input?, fn) 种子和一次性剧本中
环境相关性框架被限定为回调 :
await server.runServiceContext(async (ctx) => {
await runSeeds({ seeds, ctx });
});
server.createServiceContext(input?) 仍可供长期服务器使用,
并测试拥有周围合成寿命的过程.
原始路线
server.rawRoute({ name, method, path, metadata, hooks }).handle(fn) 构建获取
处理无法成为合同的线路的操作者——第三方召回,
签名验证的 webhoks, 流出端点—— 仍然运行整个
服务器管道:钩子、上下文创建、仪器和框架
错误映射 。 请求解析被跳过 身体保持未消费
处理器; metadata 输入元数据驱动钩钩, 如限制速率
就像合同元数据一样 原始路线不添加到路线中
注册 – 在路由自己的路径上挂载返回的处理器 .
对于服务器事件,返回
createServerSentEventResponse(...) 从 @beignet/core/server。帮助者
拥有可移植 SSE 框架、 JSON 编码、心跳、请求终止
req.signal,订阅清理,1 MiB 未读数据限制,可选
最大连接寿命,以及反缓冲头。 这个 start(...)
c. 调用接收可同步设置的流视信号;
预期 AbortError 因溪流关闭导致的拒绝作为
取消 。 该应用程序仍然拥有授权,重放,连接限制,
和解.
HTTP 操作处理器
@beignet/web 用品 createHealthRoute, (中文(简体) ), createStorageRoute, (中文(简体) ),
createUploadRoute, (中文(简体) ), createWebhookRoute, (中文(简体) ), createPaymentWebhookRoute, (中文(简体) ),
createOutboxDrainRoute, (中文(简体) ), createScheduleRoute, (中文(简体) ), createOpenAPIHandler,以及
createSwaggerUIHandler。这些使用标准 Request 财务报告和审定财务报表 Response 对象.
下一个应用程序通过相同的执行 @beignet/next。 。 。 .
在明确的主机路径上挂载返回的方法处理器 。 创建处理器
不注册到 server.fetch,将其添加到合同登记处,或
将其列入合同衍生的OpenAPI。 不支持的方法返回 405 与
Allow; 准备状态和文件
回应。 排程和发箱触发器只接受GET和POST.
例如,在Bun主机上挂载一个有界限、经认证的排水管:
import { createOutboxDrainRoute } from "@beignet/web";
import { createAppServer } from "@/server";
import { outboxRegistry } from "@/server/outbox";
import { env } from "@/lib/env";
const server = await createAppServer();
const drain = createOutboxDrainRoute({
server,
registry: outboxRegistry,
secret: env.CRON_SECRET,
batchSize: 50,
pipeline: { path: "/api/cron/outbox/drain" },
});
Bun.serve({
fetch(request) {
if (new URL(request.url).pathname === "/api/cron/outbox/drain") {
return request.method === "GET" ? drain.GET(request) : drain.POST(request);
}
return server.fetch(request);
},
});
应用程序必须绑定 outbox 以及其登记处使用的发送端口。 留着
主机的关闭处理和呼叫 server.stop() 当它停止。 时间表
处理人员同样执行每个经认证的请求的登记时间表;
帮助者都没有启动背景循环 。 失踪的秘密被关闭
在加载服务器或创建上下文之前,拒绝未经授权的请求 .
Webhook,支付-webhook,发件箱,调度处理器使用服务器的
rawRoute 保留其钩子、元数据、上下文,
出错映射和仪表. pipeline.path 识别挂载
路径到钩。 最小值 RouteServer<Ctx> 测试双人只能曝光
createRequestContext,在此情况下,帮助者不直接执行
钩子. 保健、公共储存和文件处理人员是独立的;
上传授权和上下文属于上传路由器.
Webhooks 绑定并抓取生体后再装入服务器. 核查 即使上下文代码读取了请求 也会收到被俘尸体 付款 webhooks 在读取身体前需要签名头. 使用运行时间 由安装的提供者 SDK 支持; 仅获取处理器并不意味着 边缘相容性.
对于上传,请通过静态 { uploadName, action } 选项或主机解析
参数改为: POST(request, { uploadName, action })。网络处理器不会
从 URL 中推断参数。 下一个 createUploadRoute 调整其 App 路由器
context.params 给同一个处理器。 从当前构建请求上下文
构建上传路由器时请求; 绝不重用一个用户的上下文
后上传.
createHealthRoute(serverOrLoader, healthConfig, environment?) 返回时 GET
与 Cache-Control: no-store。 。 。 . createStorageRoute(storageOrLoader, options?)
返回时 GET 财务报告和审定财务报表 HEAD 且只服务于公共物品; 私人和缺失
对象均返回 404。 。 。 .
私人下载
打电话 createStorageDownloadResponse({ storage, key, fileName?, method? }) 内部
在其使用大小写解决和授权文件后认证的路径
记录。 从这个包中导入它 。 将房客和房主的检查保留在
; 帮助者不授权访问或从密钥推断所有权.
它返回了串行附件 Response,或 null 缺少字节,所以
路由可以丢出其应用目录出错. 存储失败 。 答复使用情况
Cache-Control: private, no-store, (中文(简体) ), X-Content-Type-Options: nosniff,以及
Referrer-Policy: no-referrer。 。 。 . fileName 默认键的最后一段;
Unicode 文件名被保留,而空名、路径分隔符和
控制字符被拒绝。 内容类型、长度和修改时间
从存储,有一个 application/octet-stream 内容类型倒置.
将原始上传文件名规范化,然后将其传递给帮助者 .
method 默认为 "GET"· ; "HEAD" 发送信头并释放未读出的身体.
帮助者不执行范围请求或有条件的GET. 登记
通过应用程序的正常合同或原始路线管道进行认证
钩而跑. createStorageRoute(...) 仅服务于公共对象.
直接存储对象的下载, 先授权然后呼叫
storage.temporaryUrl(key, { expiresInSeconds, fileName })处理 ready, (中文(简体) ),
not_found,以及 unsupported 明确; 不支持的提供者可以使用此选项
流来相助. 见 私人下载指南
用于路由组成和临时链接语义.
推迟交货
createOutboxDrainTrigger({ defer, createContext, registry, ... }) 这是
可选主机排程,最佳交付优化. 以后再打电话
一个成功的交易,提供主机的延迟工作原始,并保持
耐久或工人排水恢复。 下一个通过它
createNextOutboxDrainTrigger({ defer: after, ... })HTTP排出和排出时间
使用错误报告 beignet.outbox.http 财务报告和审定财务报表 beignet.schedule.http; 推迟
排水沟的使用 beignet.outbox.deferred。默认调度运行源是
http-schedule-route; 应用程序可以设置 source 当然.
低级助手
import {
createFetchHandler,
toRequestLike,
toWebResponse,
webFetchAdapter,
} from "@beignet/web";
createFetchHandler(server)适应 Beignet 服务器实例或 API 处理器 改为(req: Request) => Promise<Response>。 。 。 .toRequestLike(req)转换标准Request给Beignet的 框架中性请求形状,保留本地请求及其中止 信号.toWebResponse(response)将 Beignet 响应转换为标准Response,将数组值响应标题中的每个项目作为附件 单独的字段。 用于重复字段, 如Set-Cookie。 。 。 .webFetchAdapter是混凝土HttpAdapter<Request, Response>用于 Web 获取运行时间的执行 .
土著 Response 处理者返回的示例保持不变.
明确退回 ReadableStream 也传递给当地人
不变,包括流式 application/json 和结构化 +json
回应。 其他平庸的Beignet响应在
Content-Type 缺席, application/json,或结构化 +json 媒体
类型。 故意返回文本或二进制数据,设置明确的非JSON
Content-Type 返回网络 BodyInit 值 :
return {
status: 200,
headers: { "content-type": "text/plain; charset=utf-8" },
body: "hello",
};
使用本地语言 Response 当运输特定行为如方向调整或
多段边界生成需要直接Web API控制.
适应合同
@beignet/core/server 拥有框架行为: 路由匹配、 钩子、 请求
验证、反应解析、误差映射、反应所有权和提供者
生命周期。 对于框架中立的路由响应,解析的响应-shema
输出成为主体 @beignet/web 连载. @beignet/web 拥有 Web 获取边缘转换和共享可操作的 HTTP
管理者.
路由器生成的 404/405 回复跳过请求解析和应用上下文
创建. onRequest, (中文(简体) ), beforeSend,以及 afterSend 仍在运行,包括
关联、 CORS 和安全头; 接收响应钩
ctx: undefined。在自定义钩子中提供上下文访问。 405美元包括: Allow, (中文(简体) ),
404 运行上下文和授权
通常情况下.
导出 webFetchAdapter 执行正式的核心适配器合同:
import type { HttpAdapter } from "@beignet/core/server";
export const adapter: HttpAdapter<Request, Response> = webFetchAdapter;
对具有非 Fetch 本地请求的未来运行时间适配器使用相同的形状 或反应类型.
测试
使用 @beignet/web/testing 通过同一 Web 抓取来练习路径
不打开网络端口的适配器 。 使用 @beignet/core/testing 旁边
当路由需要应用样式上下文并测试端口时.
import { createTestContextFactory, createTestPorts } from "@beignet/core/testing";
import { createTestApp } from "@beignet/web/testing";
import { getTodo } from "./features/todos/contracts";
import { routes } from "./server/routes";
const fixture = createTestPorts<AppContext["ports"]>({
base: initialPorts,
overrides: { todos: createInMemoryTodoRepository() },
});
const createContext = createTestContextFactory<AppContext, AppContext["ports"]>({
ports: fixture.ports,
});
const app = await createTestApp({
ports: fixture.ports,
routes,
context: () => createContext(),
});
const todo = await app.request(getTodo, {
path: { id: "todo_1" },
});
app.request(contract, args) 使用与
@beignet/core/client时 app.safeRequest(contract, args) 返回键入
成功/错误的结果而不是扔出.
createTestApp(...) 应用两个方便测试的默认值,以及明确
选项总是赢 :
onUnboundPorts默认为"ignore",所以应用与延迟提供者 端口启动, 不安装每个提供者 。 仍然读取一个未绑定的端口 抛出使用。 制作服务器createFetchServer(...)保留 严格"error"默认.mapUnhandledError默认为返回的映射器{ status: 500, body: { code: "INTERNAL_SERVER_ERROR", message: err.message } }, (中文(简体) ), 因此失败的测试显示的是真实的错误消息而不是通用的响应.
元数据驱动的行为需要两分之一的线:
metadata.rateLimit 或者说 metadata.idempotency 只有在
匹配钩子( r)createRateLimitHooks(...) 编号 : createIdempotencyHooks(...)(中文(简体) )
在测试应用程序中 hooks 财务报告和审定财务报表 其端口已约束. createTestApp(...)
当注册合同宣布应用程序不能行为时提醒创建
执行,所以"429从未发生过" 响亮的失败而不是沉默
路过. createTestPorts(...) 从 @beignet/core/testing 已绑定
内存 rateLimit 财务报告和审定财务报表 idempotency 端口,所以通过应用程序的钩是
通常是唯一缺失的一块 :
const app = await createTestApp({
ports: fixture.ports,
context: appContext,
trustedProxy: { clientIp: "x-forwarded-for-last" },
hooks: [
createRateLimitHooks(),
createIdempotencyHooks(),
],
routes,
});
// Declared limits now produce real 429s...
const responses = await Promise.all(
Array.from({ length: 61 }, () => app.fetch("http://beignet.test/api/search")),
);
expect(responses.at(-1)?.status).toBe(429);
// ...and repeated idempotency keys replay instead of re-running the handler.
const replay = await app.fetch("http://beignet.test/api/todos", {
method: "POST",
headers: { "idempotency-key": "todo-1", "content-type": "application/json" },
body: "{}",
});
expect(replay.headers.get("idempotency-replayed")).toBe("true");
声明其背景蓝图的应用程序 defineServerContext(...) 内
server/context.ts 可以将相同的值传递给运行时服务器和
createTestApp(...)数字 :
import { appContext } from "../server/context";
const app = await createTestApp({ ports, routes, context: appContext });
基础 app.server 保留蓝图的服务输入类型,所以
测试可以调用 app.server.runServiceContext(input?, fn) 用于非HTTP工作.
使用 createTestRequester(...) 当一个测试套房重复同样的认证,
相关性,或请求形状标题 :
import { createTestApp, createTestRequester } from "@beignet/web/testing";
const app = await createTestApp({ ports, routes, context: createContext });
const authedRequest = createTestRequester(app, {
headers: {
"x-user-id": "user_1",
"x-request-id": "req_1",
},
});
const todo = await authedRequest.request(getTodo, {
path: { id: "todo_1" },
});
createFetchServer(...) 财务报告和审定财务报表 createTestApp(...) 接受相同的建议
服务器级别 trustedProxy 政策。 请求上下文厂和服务器
钩子收到一个解开 requestInfo; 转发头仍然不可信
当该选项被省略时.
类型广播
createBroadcastRoute 从 @beignet/web 使用 SSE 显示授权频道
服务器上下文、元数据、路由钩以及 HTTP 错误管道。 原始路线
还接受 hooks;这些钩子所添加的字段在其处理器中被推断出.
// server/broadcast-route.ts
import { createBroadcastRoute, type FetchServer } from "@beignet/web";
import type { AppContext } from "@/app-context";
import { channels } from "@/server/broadcasts";
export function createAppBroadcastRoute(server: FetchServer<AppContext>) {
return createBroadcastRoute({
server,
channels,
maxLifetimeMs: 60_000,
});
}
CLI网络简介生成了这家工厂. 通过您组装的服务器
应用程序 createAppServer() 到它和路线 /api/broadcasts 返回时
处理者。 例如, 将此发送合并到 Bun 主机的“ 获取” 条目 :
import { createAppServer } from "@/server";
import { createAppBroadcastRoute } from "@/server/broadcast-route";
const server = await createAppServer();
const broadcasts = createAppBroadcastRoute(server);
Bun.serve({
idleTimeout: 60,
fetch(request) {
if (new URL(request.url).pathname === "/api/broadcasts") {
return broadcasts.GET(request);
}
return server.fetch(request);
},
});
保持您的应用程序的现有关闭处理和呼叫 server.stop() 当
主机停止。 广播处理器返回 405 用于非GET请求.
登记每个频道,明确授权具有约束力,包括公共
频道。 过 hooks: [auth.required()] 当您的应用程序拥有了这个钩子时; 否则
在约束中申明身份. server 接受一个实例或一个懒惰的装载器.
选项还包括 metadata, (中文(简体) ), path (默认) /api/broadcasts),以及
resolveOrigin({ ctx, request }) (a) 将启动客户排除在外.
挂载 GET 在已配置的路径中. maxLifetimeMs 默认为 60_000 财务报告和审定财务报表
接受正安全整数 1 结束 3_600_000 (一个小时).
对于允许5分钟请求的主机,请使用 maxLifetimeMs: 240_000 财务报告和审定财务报表
留下剩下的一分钟进行设置/清理。 更长的流减少更新
和调节频率,但增加授权间隔
检查。 每个新连接都会重新检查访问权限.
浏览器以五秒钟的监察优雅 来纪念公布的一生
期间。 它的最后期限涵盖整个连接; 以后的准备状态信息
心跳不会延长 心跳和初期准备暂停
独立。 每条准备信息都必须包括有效的 maxLifetimeMs· ;
缺少或无效的寿命 。 正常过期发送 a
关闭前的连接宽更新控制框架 。 计划续约
触发 onSync。 。 。 .
全球钩子故障保留了它们的HTTP状态. 每道拒绝返回 控制帧; 临时 429/5xx 失败可重试 。 客户端必须调试 在准备/重新连接后,因为广播没有持久的重播。 用这个 流式抓取,支持动态头,多轴最多可达20个频道. 当出版工作和流处理器运行在不同的过程中时使用 Redis.
对于没有服务器的主机,允许流出和输出提供者连接并设置 设置/清理头室. 60秒后,主机请求截止时间为120秒出站; 通过主机的设置配置此选项, 但须遵守其限制 。 获取 适配器不会使节点专用提供者与Edge 运行时兼容. 见 广播指南。 。 。 .
将主机和代理机闲置超时超过25秒心跳间隔. 这个
Bun 实例使用 idleTimeout: 60; Bun 默认的 10 秒闲置超时也
用于静音广播流的简写. 禁用此端点的代理缓冲 .
保留连接槽
新选择 admit({ ctx, request, signal }) 钩子每个物理运行一次
上下文/认证钩后和SSE前的多轴连接
反应。 它可能返回释放函数(同步或同步),或
没什么 丢弃目录错误以拒绝整个 HTTP 请求 。 HTTP 429和
5xx回复可重新测试; Retry-After 被浏览器所尊重。 入学
不取代每个频道的独立授权.
此示例使用已有的 LocksPort 执行 应用程序拥有
三分机政策. 声明 locks: LocksPort 内 AppPorts 把锁系上
提供者在 server/providers.ts。内存锁限制一个进程;已分发
限制需要一个共享的原子租赁商店。 Redis是一个选项,而不是一个要求.
添加 BroadcastConnectionLimit 改为 features/shared/errors.ts 有状态 429, (中文(简体) ),
代码 BROADCAST_CONNECTION_LIMIT和消息 Too many broadcast connections。 。 。 .
将策略放入 server/broadcast-admission.ts数字 :
import type { LocksPort } from "@beignet/core/locks";
import { appError } from "@/features/shared/errors";
export async function reserveBroadcastConnection({
locks, userId, signal,
}: { locks: LocksPort; userId: string; signal: AbortSignal }) {
for (let slot = 0; slot < 3; slot++) {
signal.throwIfAborted();
const result = await locks.acquire(`broadcast:${userId}:${slot}`, {
ttlMs: 300_000,
waitMs: 0,
});
if (result.acquired) {
// Return ownership even if cancelled while awaiting acquisition.
return async () => { await result.lease.release(); };
}
}
throw appError("BroadcastConnectionLimit", { headers: { "Retry-After": "5" } });
}
在终点上注册 server/broadcast-route.ts。此示例
假设在 ctx.auth· ; requireUserId(ctx)
购入前声称:
import { createBroadcastRoute, type FetchServer } from "@beignet/web";
import type { AppContext } from "@/app-context";
import { auth } from "@/lib/route-auth";
import { requireUserId } from "@beignet/core/ports";
import { reserveBroadcastConnection } from "@/server/broadcast-admission";
import { channels } from "@/server/broadcasts";
export function createAppBroadcastRoute(server: FetchServer<AppContext>) {
return createBroadcastRoute({
server, channels, hooks: [auth.required()], maxLifetimeMs: 240_000,
admit: ({ ctx, signal }) => reserveBroadcastConnection({
locks: ctx.ports.locks, userId: requireUserId(ctx), signal,
}),
});
}
挂载返回 GET 时间 /api/broadcasts。东道主拥有其请求
持续时间设定; 允许此四分钟的流量使用五分钟 .
Beignet在到期后调用每个已获取的发布功能, 请求/ body
取消、 提供者关闭 , 或失败的流设置 。 如果只是一些频道
失败时,它们的资源会在接受的频道保留连接时释放.
如果在取消或设立十秒录取后完成收购
超时,其返回的资源立即被放出. 过 signal 改为:
支持时获取 API; 示例在尝试中检查, 然后
返回成功的获取,即使等待取消时发生了
这个 获取后抛出而不返回释放会泄露资源.
清理失败发出 broadcast.cleanup-error 不跳过其他发布.
不需要应用程序流事件听众 。 每次续约都新鲜
录取和批准. 分配的租约仍需期满 因为
程序突然终止无法进行清理 。 将 TTL 租赁在溪流之上
寿命加购置/设置/清理前厅;此示例对等 a
5分钟的租借 4分钟的溪流。 托管请求的限制也必须
离开头室。 保留储存、槽数、过期和拒绝政策
应用程序拥有;框架没有强制设置连接限制存储.