跳转到内容

createScope() 的长期边界

有些 shajara 工作不是由另一段 routine 启动的,而是由应用入口收到的事件触发:HTTP 服务收到请求,worker 收到消息,UI shell 收到事件。这个入口通常比单次回调 活得更久,因此需要一个可以承接这些 routine 的长期 scope。

createScope() 用来在普通应用代码中打开这个边界。外部回调从它启动 routine;入口退出 时,它收拢仍属于自己的工作。

下面的 Hono 服务在启动流程中创建 serviceScope。收到请求后,route handler 从这个 scope 启动请求 routine;收到 shutdown signal 后,顶层流程离开等待,await using 释放 HTTP server 和 shajara scope。

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { CanceledError, createScope, until } from "@shajara/host";
try {
await using serviceScope = createScope();
const app = new Hono();
app.get("/reports/:id", (c) =>
serviceScope.run(function* handleReportRequest() {
const reportId = c.req.param("id");
const response = yield* until(() => fetch(`https://reports.internal/reports/${reportId}`));
const report = yield* until(() => response.json());
return c.json(report);
}),
);
await using _server = serve({ fetch: app.fetch, port: 3000 });
const shutdown = Promise.withResolvers<void>();
process.once("SIGINT", shutdown.resolve);
process.once("SIGTERM", shutdown.resolve);
await Promise.race([shutdown.promise, serviceScope.closed]);
} catch (error) {
if (!(error instanceof CanceledError)) {
console.error(error);
process.exitCode = 1;
}
}

serviceScope 位于启动流程,而不是 handler 内部。每次 handler 被调用时,都会回到这个 长期 scope。

createScope() 返回的 scope 可以在 routine 外部调用 run(...)。route handler 把这个 Promise 返回给 Hono;shajara 则通过同一次 run(...) 观察 handleReportRequest 的 结果。

app.get("/reports/:id", (c) =>
serviceScope.run(function* handleReportRequest() {
const reportId = c.req.param("id");
const response = yield* until(() => fetch(`https://reports.internal/reports/${reportId}`));
const report = yield* until(() => response.json());
return c.json(report);
}),
);

这次请求工作属于 serviceScope。routine 正常返回时,handler 拿到的 Promise 会 resolve 为 c.json(report)。如果 routine 让非取消异常逃出,这个 Promise 通常会 reject 为 ScopeError,同一个失败也会关闭 serviceScope

当错误只应该影响当前请求时,在 routine 内部把它处理成 HTTP response。让错误逃出 routine,表示这个失败属于拥有它的长期 scope。

serviceScope 和 HTTP server 位于同一个顶层 try block。这个 scope 不随单次请求 创建或销毁;顶层流程还在等待时,它保持打开。

try {
await using serviceScope = createScope();
const app = new Hono();
app.get("/reports/:id", (c) =>
serviceScope.run(function* handleReportRequest() {
/* ... */
}),
);
await using _server = serve({ fetch: app.fetch, port: 3000 });
/* ... */
}

await using 让 scope 的释放跟随入口生命周期。服务离开 block 时,HTTP server 停止接收 新请求,serviceScope 也开始关闭;仍属于它的请求工作会跟着这个 scope 收敛。

打开的 scope 会让 host scheduler 保持活跃,因此即使当前没有可运行的请求,Node.js 进程 也无法自然退出。这个 scope 仍可供后续回调启动工作。关闭应用入口时,应取消或异步释放 它,而不是强制退出进程。独立的 run(...) 在其 scope settled 后不再持有 scheduler 资源,让已经完成的一次性命令可以自然退出。

createScope() 暴露 closed Promise,让应用代码可以观察长期 scope 的收敛结果。服务 运行期间,顶层流程既等待 shutdown signal,也等待 serviceScope.closed

const shutdown = Promise.withResolvers<void>();
process.once("SIGINT", shutdown.resolve);
process.once("SIGTERM", shutdown.resolve);
await Promise.race([shutdown.promise, serviceScope.closed]);

收到 shutdown signal 后,顶层代码离开 try block,await using 释放 HTTP server 和 serviceScope。如果 serviceScope 因未捕获的请求失败,或其他属于这个长期边界的失败 而先关闭,Promise.race(...) 会把关闭结果交给外层 catch

} catch (error) {
if (!(error instanceof CanceledError)) {
console.error(error);
process.exitCode = 1;
}
}

await using 发起的取消会作为成功清理完成,因此正常 shutdown 不会进入 catch。如果 serviceScope.closed 先 reject,catch 会忽略预期的 CanceledError,并报告其他失败。