@beignet/web

中文文档
0.0.65发布于 2026/11/08MITTypeScriptES Module近 30 天下载 2011

Beignet 的 Web 获取适配器

中文使用文档
文档目录14 节
  1. 安装
  2. 代理技能
  3. 快开始
  4. 阿宝
  5. 服务工作
  6. 原始路线
  7. HTTP 操作处理器
  8. 私人下载
  9. 推迟交货
  10. 低级助手
  11. 适应合同
  12. 测试
  13. 类型广播
  14. 保留连接槽

@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分钟的溪流。 托管请求的限制也必须 离开头室。 保留储存、槽数、过期和拒绝政策 应用程序拥有;框架没有强制设置连接限制存储.

版本与发布记录

@beignet/web 共发布 63 个版本,首次发布于 2026/06/01;过去 12 个月发布了 63 个版本。

版本发布日期距今
0.0.65latest2026/11/08今天
0.0.642026/11/034 天前
0.0.632026/11/026 天前
0.0.622026/11/017 天前
0.0.612026/11/017 天前
0.0.602026/11/017 天前

关于 @beignet/web 的常见问题

@beignet/web 是什么?

Beignet 的 Web 获取适配器

如何安装 @beignet/web?

在项目目录运行 npm install @beignet/web,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@beignet/core(>=0.0.3 <1.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@beignet/web 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@beignet/web 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@beignet/web 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@beignet/web 可以商用吗?

@beignet/web 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@beignet/web 的下载量有多少?

根据 npm 官方下载统计,@beignet/web 在 2026/10/08 – 2026/11/06 这 30 天内被下载 2,011 次,日均约 67 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@beignet/web 还在维护吗?

最新版本 0.0.65 发布于 2026/11/08(今天);至今共发布 63 个版本,其中过去 12 个月发布了 63 个;首次发布于 2026/06/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。