@faapi/faapi 6.21.0 → 6.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -762,7 +762,7 @@ type TaskFailedHandler = (info: TaskFailedInfo) => Promise<void> | void;
762
762
  *
763
763
  * 核心导出:
764
764
  * - `encodeSseEvent(event)`:把 SSE 事件对象编码为符合 HTML5 SSE 规范的字符串
765
- * - `createSseWriter()`:创建一个 SseWriter,封装 ReadableStream + Response,提供 send/close/sendError API
765
+ * - `createSseWriter()`:创建一个 SseWriter,封装 ReadableStream + Response,提供 send/close/onClose/sendError API
766
766
  * - `SseWriter`:writer 类型,ctx.sse() 返回此类型
767
767
  *
768
768
  * 设计要点:
@@ -803,6 +803,20 @@ interface SseEvent {
803
803
  /** 注释行(以 : 开头),用于 keep-alive 心跳 */
804
804
  comment?: string;
805
805
  }
806
+ /**
807
+ * ctx.sse() 的选项
808
+ */
809
+ interface SseOptions {
810
+ /**
811
+ * 声明流生命周期独立于 handler 返回(长连接订阅模式)
812
+ *
813
+ * `true` 时 handler 返回后框架不自动 close——handler 只负责把连接挂到推送源
814
+ * (事件总线、change stream),注册完立即返回,连接长存直到显式 close()、
815
+ * 客户端断开或 handler 抛错兜底。清理推送源用 `onClose(callback)`。
816
+ * 默认 false(handler 返回时框架自动 close 兜底)。
817
+ */
818
+ keepOpen?: boolean;
819
+ }
806
820
  /**
807
821
  * SSE writer:封装流式推送 API
808
822
  *
@@ -827,10 +841,20 @@ interface SseWriter {
827
841
  sendError(error: unknown): void;
828
842
  /** 关闭流(多次调用安全) */
829
843
  close(): void;
844
+ /**
845
+ * 注册流结束回调
846
+ *
847
+ * 显式 close、框架兜底 close、客户端断开、sendError 任一路径结束流时恰好触发
848
+ * 一次(多个回调按注册顺序执行,回调自身抛错被忽略);注册时流已结束则立即触发。
849
+ * keepOpen 模式下用于退订/清理推送源,防止客户端断开后订阅随重连累积泄漏。
850
+ */
851
+ onClose(callback: () => void): void;
830
852
  /** 流是否已关闭(handler 主动 close 或框架自动 close) */
831
853
  readonly closed: boolean;
832
854
  /** 客户端是否已断开(ReadableStream 被 cancel) */
833
855
  readonly aborted: boolean;
856
+ /** 创建时是否声明了 `{ keepOpen: true }`(handler 返回后框架不自动 close) */
857
+ readonly keepOpen: boolean;
834
858
  /**
835
859
  * 流缓冲背压状态(透传 controller.desiredSize)
836
860
  *
@@ -1061,8 +1085,12 @@ interface FaapiContext {
1061
1085
  * sse.close();
1062
1086
  * }
1063
1087
  * ```
1088
+ *
1089
+ * @param options `{ keepOpen: true }` 声明流生命周期独立于 handler 返回(长连接
1090
+ * 订阅模式):handler 返回后框架不自动 close,连接长存直到显式 close()、客户端
1091
+ * 断开或 handler 抛错兜底;清理推送源用 `writer.onClose(callback)`。默认 false。
1064
1092
  */
1065
- sse(): SseWriter;
1093
+ sse(options?: SseOptions): SseWriter;
1066
1094
  /**
1067
1095
  * 显式包装成功响应(返回 Response 对象)
1068
1096
  *
@@ -1313,4 +1341,4 @@ interface RouteInfo {
1313
1341
  output: RouteOutputSchema | null;
1314
1342
  }
1315
1343
 
1316
- export { type ToolRegistry as $, type AppRegistries as A, type RouteOutputSchema as B, type CorsOptions as C, type RouteParamSchema as D, type SseEvent as E, type FaapiMiddleware as F, type SseWriter as G, type HelmetOptions as H, type InjectorMap as I, type TaskContext as J, type TaskDriverJob as K, type LogConfig as L, type TaskDriverProcess as M, type TaskDriverRecord as N, type TaskFailedInfo as O, type TaskJob as P, type TaskJobStatus as Q, type RouteManifest as R, type SkillRegistry as S, type TaskClient as T, type TaskMetadata as U, type TaskModule as V, type WsRouteManifest as W, type TaskRegistriesSnapshot as X, type TaskRegistriesView as Y, type ToolCore as Z, type ToolPathMeta as _, type FaapiContext as a, cors as a0, createAppRegistries as a1, createTaskRegistriesView as a2, createTaskRegistry as a3, helmet as a4, type TaskFailedHandler as b, type TaskQueueDeps as c, type TaskQueue as d, type TaskDriver as e, type TaskRegistry as f, type TaskManifest as g, type ToolMetadata as h, type AgentCore as i, type AgentMetadata as j, type CreateLoggerOptions as k, type Logger as l, type AgentHandleFactory as m, type AgentHandleStore as n, type AgentPathMeta as o, type AgentRegistry as p, type AgentToolDescriptor as q, type FaapiContextConfig as r, type FaapiTaskMeta as s, type FailOptions as t, type Injector as u, type LogEntry as v, type LogLevel as w, type LogSink as x, type RouteInfo as y, type RouteInputSchema as z };
1344
+ export { type ToolPathMeta as $, type AppRegistries as A, type RouteOutputSchema as B, type CorsOptions as C, type RouteParamSchema as D, type SseEvent as E, type FaapiMiddleware as F, type SseOptions as G, type HelmetOptions as H, type InjectorMap as I, type SseWriter as J, type TaskContext as K, type LogConfig as L, type TaskDriverJob as M, type TaskDriverProcess as N, type TaskDriverRecord as O, type TaskFailedInfo as P, type TaskJob as Q, type RouteManifest as R, type SkillRegistry as S, type TaskClient as T, type TaskJobStatus as U, type TaskMetadata as V, type WsRouteManifest as W, type TaskModule as X, type TaskRegistriesSnapshot as Y, type TaskRegistriesView as Z, type ToolCore as _, type FaapiContext as a, type ToolRegistry as a0, cors as a1, createAppRegistries as a2, createTaskRegistriesView as a3, createTaskRegistry as a4, helmet as a5, type TaskFailedHandler as b, type TaskQueueDeps as c, type TaskQueue as d, type TaskDriver as e, type TaskRegistry as f, type TaskManifest as g, type ToolMetadata as h, type AgentCore as i, type AgentMetadata as j, type CreateLoggerOptions as k, type Logger as l, type AgentHandleFactory as m, type AgentHandleStore as n, type AgentPathMeta as o, type AgentRegistry as p, type AgentToolDescriptor as q, type FaapiContextConfig as r, type FaapiTaskMeta as s, type FailOptions as t, type Injector as u, type LogEntry as v, type LogLevel as w, type LogSink as x, type RouteInfo as y, type RouteInputSchema as z };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as FaapiContext, F as FaapiMiddleware, I as InjectorMap, R as RouteManifest, W as WsRouteManifest, C as CorsOptions, H as HelmetOptions } from './routeTypes-gyz4PMh7.js';
1
+ import { a as FaapiContext, F as FaapiMiddleware, I as InjectorMap, R as RouteManifest, W as WsRouteManifest, C as CorsOptions, H as HelmetOptions } from './routeTypes-CXWkRx4W.js';
2
2
  import { Server } from 'node:http';
3
3
  import { WebSocket } from 'ws';
4
4
 
package/dist/testing.js CHANGED
@@ -155,13 +155,28 @@ function encodeSseEvent(event) {
155
155
  return out;
156
156
  }
157
157
  function createSseWriter(options = {}) {
158
- const { onFirstWrite } = options;
158
+ const { onFirstWrite, keepOpen = false } = options;
159
159
  const encoder = new TextEncoder();
160
160
  let controller = null;
161
161
  let closed = false;
162
162
  let aborted = false;
163
163
  let firstWriteTriggered = false;
164
164
  let drainWaiters = [];
165
+ let closeCallbacks = [];
166
+ let closeCallbacksFired = false;
167
+ const safeRunCallback = (callback) => {
168
+ try {
169
+ callback();
170
+ } catch {
171
+ }
172
+ };
173
+ const runCloseCallbacks = () => {
174
+ if (closeCallbacksFired) return;
175
+ closeCallbacksFired = true;
176
+ const callbacks = closeCallbacks;
177
+ closeCallbacks = [];
178
+ for (const callback of callbacks) safeRunCallback(callback);
179
+ };
165
180
  const resolveDrainWaiters = () => {
166
181
  const waiters = drainWaiters;
167
182
  drainWaiters = [];
@@ -185,6 +200,7 @@ function createSseWriter(options = {}) {
185
200
  aborted = true;
186
201
  closed = true;
187
202
  controller = null;
203
+ runCloseCallbacks();
188
204
  resolveDrainWaiters();
189
205
  }
190
206
  },
@@ -227,6 +243,7 @@ function createSseWriter(options = {}) {
227
243
  close() {
228
244
  if (closed) return;
229
245
  closed = true;
246
+ runCloseCallbacks();
230
247
  resolveDrainWaiters();
231
248
  if (controller) {
232
249
  try {
@@ -236,6 +253,13 @@ function createSseWriter(options = {}) {
236
253
  controller = null;
237
254
  }
238
255
  },
256
+ onClose(callback) {
257
+ if (closeCallbacksFired) {
258
+ safeRunCallback(callback);
259
+ return;
260
+ }
261
+ closeCallbacks.push(callback);
262
+ },
239
263
  get closed() {
240
264
  return closed;
241
265
  },
@@ -252,6 +276,9 @@ function createSseWriter(options = {}) {
252
276
  get aborted() {
253
277
  return aborted;
254
278
  },
279
+ get keepOpen() {
280
+ return keepOpen;
281
+ },
255
282
  get response() {
256
283
  return response;
257
284
  }
@@ -633,10 +660,15 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
633
660
  * 接入底层连接——流式期间字节即产即达(见 createServer.md「SSE 提前接管」)。
634
661
  * 无钩子场景(app.inject / 测试直调 invokeHandler)回落为 handler 返回后交出。
635
662
  *
663
+ * `{ keepOpen: true }` 声明流生命周期独立于 handler 返回:handler 返回后框架
664
+ * 不自动 close(长连接订阅模式,清理推送源用 writer.onClose);handler 抛错
665
+ * 的兜底 close 不受影响。详见 runtime/sse.md「长连接订阅模式」。
666
+ *
636
667
  * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
637
668
  */
638
- sse() {
669
+ sse(options) {
639
670
  const writer = createSseWriter({
671
+ keepOpen: options?.keepOpen,
640
672
  onFirstWrite: (response) => {
641
673
  ctx.__earlyRespond?.(response);
642
674
  }
@@ -1265,7 +1297,7 @@ async function invokeHandler(handler, ctx, body, middlewares, injectors) {
1265
1297
  const pickSseAndAutoClose = () => {
1266
1298
  const sseWriter = ctx.__sseWriter;
1267
1299
  if (!sseWriter) return null;
1268
- if (!sseWriter.closed && !sseWriter.aborted) {
1300
+ if (!sseWriter.keepOpen && !sseWriter.closed && !sseWriter.aborted) {
1269
1301
  sseWriter.close();
1270
1302
  }
1271
1303
  return mergeMeta(sseWriter.response, meta);