@dangao/bun-server 2.2.0 → 2.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-provider.d.ts","sourceRoot":"","sources":["../../../src/ai/providers/anthropic-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG9E,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yBAAyB;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,iBAAkB,YAAW,WAAW;IACnD,SAAgB,IAAI,eAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAEvB,MAAM,EAAE,uBAAuB;IAOrC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA+DvD,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IA8EtD,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;YAInC,IAAI;CAiBnB"}
1
+ {"version":3,"file":"anthropic-provider.d.ts","sourceRoot":"","sources":["../../../src/ai/providers/anthropic-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG9E,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yBAAyB;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,qBAAa,iBAAkB,YAAW,WAAW;IACnD,SAAgB,IAAI,eAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;gBAEvB,MAAM,EAAE,uBAAuB;IAOrC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA+DvD,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IAgFtD,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;YAInC,IAAI;CAkBnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"google-provider.d.ts","sourceRoot":"","sources":["../../../src/ai/providers/google-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG9E,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,cAAe,YAAW,WAAW;IAChD,SAAgB,IAAI,YAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAEd,MAAM,EAAE,oBAAoB;IAMlC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAoEvD,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IAkEtD,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;IAIjD,OAAO,CAAC,gBAAgB;CAiBzB"}
1
+ {"version":3,"file":"google-provider.d.ts","sourceRoot":"","sources":["../../../src/ai/providers/google-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG9E,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,cAAe,YAAW,WAAW;IAChD,SAAgB,IAAI,YAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAEd,MAAM,EAAE,oBAAoB;IAMlC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAqEvD,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IAoEtD,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;IAIjD,OAAO,CAAC,gBAAgB;CAiBzB"}
@@ -1 +1 @@
1
- {"version":3,"file":"ollama-provider.d.ts","sourceRoot":"","sources":["../../../src/ai/providers/ollama-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG9E,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,cAAe,YAAW,WAAW;IAChD,SAAgB,IAAI,YAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAEnB,MAAM,GAAE,oBAAyB;IAKvC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAuCvD,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IA+DtD,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;CAGlD"}
1
+ {"version":3,"file":"ollama-provider.d.ts","sourceRoot":"","sources":["../../../src/ai/providers/ollama-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG9E,MAAM,WAAW,oBAAoB;IACnC,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2BAA2B;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,cAAe,YAAW,WAAW;IAChD,SAAgB,IAAI,YAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAEnB,MAAM,GAAE,oBAAyB;IAKvC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAwCvD,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IAiEtD,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;CAGlD"}
@@ -1 +1 @@
1
- {"version":3,"file":"openai-provider.d.ts","sourceRoot":"","sources":["../../../src/ai/providers/openai-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG9E,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sBAAsB;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7D;AAwCD,qBAAa,cAAe,YAAW,WAAW;IAChD,SAAgB,IAAI,YAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoD;gBAEzD,MAAM,EAAE,oBAAoB;IAOlC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA4CvD,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IA+EtD,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;YAKnC,IAAI;IA4BlB,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,YAAY;CAKrB"}
1
+ {"version":3,"file":"openai-provider.d.ts","sourceRoot":"","sources":["../../../src/ai/providers/openai-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG9E,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sBAAsB;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC7D;AAwCD,qBAAa,cAAe,YAAW,WAAW;IAChD,SAAgB,IAAI,YAAY;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoD;gBAEzD,MAAM,EAAE,oBAAoB;IAOlC,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA4CvD,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IAiFtD,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;YAKnC,IAAI;IA6BlB,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,YAAY;CAKrB"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/ai/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,eAAe,EACf,SAAS,EACV,MAAM,SAAS,CAAC;AAIjB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D;;;GAGG;AACH,qBACa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAA6B;gBAGd,OAAO,EAAE,eAAe;IAY3D;;OAEG;IACI,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAIpD;;OAEG;IACU,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA8B9D;;OAEG;IACI,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IAK7D;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;IAKjD;;OAEG;IACI,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW;IAU9C;;OAEG;IACI,gBAAgB,IAAI,MAAM,EAAE;YAIrB,cAAc;IA+B5B,OAAO,CAAC,WAAW;CASpB"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/ai/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,eAAe,EACf,SAAS,EACV,MAAM,SAAS,CAAC;AAIjB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAG1D;;;GAGG;AACH,qBACa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkC;IAC5D,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAC1C,OAAO,CAAC,YAAY,CAA6B;gBAGd,OAAO,EAAE,eAAe;IAY3D;;OAEG;IACI,eAAe,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI;IAIpD;;OAEG;IACU,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA8B9D;;OAEG;IACI,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC;IAK7D;;OAEG;IACI,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM;IAKjD;;OAEG;IACI,WAAW,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW;IAU9C;;OAEG;IACI,gBAAgB,IAAI,MAAM,EAAE;YAIrB,cAAc;IA+B5B,OAAO,CAAC,WAAW;CAqBpB"}
@@ -41,6 +41,11 @@ export interface AiRequest {
41
41
  tools?: AiToolDefinition[];
42
42
  /** Provider name override */
43
43
  provider?: string;
44
+ /**
45
+ * Abort signal — pass `ctx.signal` to cascade client disconnection
46
+ * to the upstream AI API, stopping token consumption immediately.
47
+ */
48
+ signal?: AbortSignal;
44
49
  }
45
50
  /**
46
51
  * Non-streaming AI response
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ai/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ai/types.ts"],"names":[],"mappings":"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"}
@@ -37,6 +37,23 @@ export interface ApplicationOptions {
37
37
  * 框架内部会自动转换为 Bun.serve 的秒单位
38
38
  */
39
39
  idleTimeout?: number;
40
+ /**
41
+ * SSE 保活配置
42
+ *
43
+ * 框架自动检测 `Content-Type: text/event-stream` 的响应,
44
+ * 对该请求禁用 Bun TCP 空闲超时(`server.timeout(req, 0)`),
45
+ * 并按配置间隔向客户端发送 SSE 注释心跳(`: keepalive\n\n`)。
46
+ *
47
+ * 心跳可防止中间代理(nginx / 云 LB)因空闲而断开连接。
48
+ *
49
+ * @default `{ enabled: true, intervalMs: 15000 }`
50
+ */
51
+ sseKeepAlive?: {
52
+ /** 是否启用,默认 true */
53
+ enabled?: boolean;
54
+ /** 心跳间隔(毫秒),默认 15000 */
55
+ intervalMs?: number;
56
+ };
40
57
  }
41
58
  /**
42
59
  * 应用主类
@@ -1 +1 @@
1
- {"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/core/application.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsB,MAAM,UAAU,CAAC;AAMzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAW3C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,CAAY;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA2B;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,uBAAuB,CAAkB;gBAE9B,OAAO,GAAE,kBAAuB;IA6BnD;;;OAGG;IACI,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAIxC;;OAEG;IACU,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoDpE;;OAEG;YACW,oBAAoB;IAqBlC;;;OAGG;YACW,sBAAsB;IAyBpC;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAqBhC;;OAEG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBlC;;;;;OAKG;IACU,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB9D;;OAEG;YACW,eAAe;IAkB7B;;;;OAIG;YACW,aAAa;IAsD3B;;;OAGG;IACI,kBAAkB,CAAC,eAAe,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAKtE;;;OAGG;IACI,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAqBrD;;;OAGG;IACH,OAAO,CAAC,kCAAkC;IAc1C;;;OAGG;IACI,wBAAwB,CAAC,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAIzE;;;OAGG;IACI,iBAAiB,CAAC,SAAS,EAAE,oBAAoB,GAAG,IAAI;IAK/D;;;OAGG;IACI,SAAS,IAAI,SAAS,GAAG,SAAS;IAIzC;;;OAGG;YACW,gBAAgB;IA0C9B;;;OAGG;YACW,kBAAkB;IAgDhC;;;OAGG;IACI,YAAY;IAInB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA2B7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAS7B"}
1
+ {"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../../src/core/application.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsB,MAAM,UAAU,CAAC;AAMzD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAW3C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE;QACb,mBAAmB;QACnB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,wBAAwB;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,CAAY;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAqB;IACxD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA2B;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8B;IACzD,OAAO,CAAC,uBAAuB,CAAkB;gBAE9B,OAAO,GAAE,kBAAuB;IA6BnD;;;OAGG;IACI,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAIxC;;OAEG;IACU,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqDpE;;OAEG;YACW,oBAAoB;IAqBlC;;;OAGG;YACW,sBAAsB;IAyBpC;;;OAGG;IACH,OAAO,CAAC,wBAAwB;IAqBhC;;OAEG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBlC;;;;;OAKG;IACU,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB9D;;OAEG;YACW,eAAe;IAkB7B;;;;OAIG;YACW,aAAa;IAsD3B;;;OAGG;IACI,kBAAkB,CAAC,eAAe,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAKtE;;;OAGG;IACI,cAAc,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI;IAqBrD;;;OAGG;IACH,OAAO,CAAC,kCAAkC;IAc1C;;;OAGG;IACI,wBAAwB,CAAC,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI;IAIzE;;;OAGG;IACI,iBAAiB,CAAC,SAAS,EAAE,oBAAoB,GAAG,IAAI;IAK/D;;;OAGG;IACI,SAAS,IAAI,SAAS,GAAG,SAAS;IAIzC;;;OAGG;YACW,gBAAgB;IA0C9B;;;OAGG;YACW,kBAAkB;IAgDhC;;;OAGG;IACI,YAAY;IAInB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA2B7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;CAS7B"}
@@ -58,6 +58,11 @@ export declare class Context {
58
58
  * 是否已解析请求体
59
59
  */
60
60
  private _bodyParsed;
61
+ /**
62
+ * 客户端断连信号
63
+ * 当客户端主动关闭连接时 abort,可用于级联取消内部请求
64
+ */
65
+ readonly signal: AbortSignal;
61
66
  constructor(request: Request);
62
67
  /**
63
68
  * 获取请求体(自动解析)
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/core/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EAAE,KAAK,eAAe,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAEhD;;;GAGG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAwC;IACnF;;OAEG;IACH,SAAgB,OAAO,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACI,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAE3B;;OAEG;IACH,SAAgB,GAAG,EAAE,GAAG,CAAC;IAEzB;;OAEG;IACH,SAAgB,MAAM,EAAE,MAAM,CAAC;IAE/B;;OAEG;IACH,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,SAAgB,KAAK,EAAE,eAAe,CAAC;IAEvC;;OAEG;IACI,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAE3C;;OAEG;IACH,SAAgB,OAAO,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACI,eAAe,EAAE,OAAO,CAAC;IAEhC;;OAEG;IACI,UAAU,EAAE,MAAM,CAAO;IAEhC;;OAEG;IACI,KAAK,EAAE,gBAAgB,EAAE,CAAM;IAEtC;;OAEG;IACH,OAAO,CAAC,KAAK,CAAC,CAAU;IAExB;;OAEG;IACH,OAAO,CAAC,WAAW,CAAkB;gBAElB,OAAO,EAAE,OAAO;IAUnC;;;OAGG;IACU,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAQxC;;;OAGG;IACH,IAAW,IAAI,IAAI,OAAO,CAEzB;IAED;;;OAGG;IACH,IAAW,IAAI,CAAC,IAAI,EAAE,OAAO,EAG5B;IAED;;;;OAIG;IACI,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI3C;;;OAGG;IACI,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAQ5C;;;;OAIG;IACI,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD;;;;OAIG;IACI,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI5C;;;;OAIG;IACI,WAAW,IAAI,MAAM;IAmB5B;;;;OAIG;IACI,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIlD;;;OAGG;IACI,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpC;;;;;OAKG;IACI,cAAc,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ;IA4BpE;;;;;OAKG;IACI,mBAAmB,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ;IAWzE,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,aAAa;CAqCtB"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/core/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EAAE,KAAK,eAAe,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAEhD;;;GAGG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAwC;IACnF;;OAEG;IACH,SAAgB,OAAO,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACI,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAE3B;;OAEG;IACH,SAAgB,GAAG,EAAE,GAAG,CAAC;IAEzB;;OAEG;IACH,SAAgB,MAAM,EAAE,MAAM,CAAC;IAE/B;;OAEG;IACH,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,SAAgB,KAAK,EAAE,eAAe,CAAC;IAEvC;;OAEG;IACI,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IAE3C;;OAEG;IACH,SAAgB,OAAO,EAAE,OAAO,CAAC;IAEjC;;OAEG;IACI,eAAe,EAAE,OAAO,CAAC;IAEhC;;OAEG;IACI,UAAU,EAAE,MAAM,CAAO;IAEhC;;OAEG;IACI,KAAK,EAAE,gBAAgB,EAAE,CAAM;IAEtC;;OAEG;IACH,OAAO,CAAC,KAAK,CAAC,CAAU;IAExB;;OAEG;IACH,OAAO,CAAC,WAAW,CAAkB;IAErC;;;OAGG;IACH,SAAgB,MAAM,EAAE,WAAW,CAAC;gBAEjB,OAAO,EAAE,OAAO;IAWnC;;;OAGG;IACU,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAQxC;;;OAGG;IACH,IAAW,IAAI,IAAI,OAAO,CAEzB;IAED;;;OAGG;IACH,IAAW,IAAI,CAAC,IAAI,EAAE,OAAO,EAG5B;IAED;;;;OAIG;IACI,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI3C;;;OAGG;IACI,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAQ5C;;;;OAIG;IACI,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD;;;;OAIG;IACI,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI5C;;;;OAIG;IACI,WAAW,IAAI,MAAM;IAmB5B;;;;OAIG;IACI,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIlD;;;OAGG;IACI,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpC;;;;;OAKG;IACI,cAAc,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ;IA4BpE;;;;;OAKG;IACI,mBAAmB,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,YAAY,GAAG,QAAQ;IAWzE,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,aAAa;CAqCtB"}
@@ -39,6 +39,14 @@ export interface ServerOptions {
39
39
  * 框架内部会转换为 Bun.serve 所需的秒
40
40
  */
41
41
  idleTimeout?: number;
42
+ /**
43
+ * SSE 保活配置
44
+ * @see ApplicationOptions.sseKeepAlive
45
+ */
46
+ sseKeepAlive?: {
47
+ enabled?: boolean;
48
+ intervalMs?: number;
49
+ };
42
50
  }
43
51
  /**
44
52
  * 服务器封装类
@@ -94,5 +102,14 @@ export declare class BunServer {
94
102
  * 获取服务器主机名
95
103
  */
96
104
  getHostname(): string | undefined;
105
+ /**
106
+ * 将 SSE Response 的 body 包裹一层心跳注入流。
107
+ *
108
+ * 原始流的数据原样透传;在数据间隙中按 intervalMs 发送
109
+ * SSE 注释帧 `: keepalive\n\n`(客户端会忽略注释帧)。
110
+ *
111
+ * 当 signal abort / 原始流结束 / 客户端断连时自动清理定时器。
112
+ */
113
+ private static wrapSseWithHeartbeat;
97
114
  }
98
115
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE1D;;OAEG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAE7C;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAgB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAa;gBAElB,OAAO,EAAE,aAAa;IAIzC;;OAEG;IACI,KAAK,IAAI,IAAI;IAsHpB;;OAEG;IACI,IAAI,IAAI,IAAI;IAWnB;;;;;OAKG;IACU,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2C9D;;OAEG;IACI,iBAAiB,IAAI,MAAM;IAIlC;;OAEG;IACI,mBAAmB,IAAI,OAAO;IAIrC;;;OAGG;IACI,SAAS,IAAI,MAAM,CAAC,uBAAuB,CAAC,GAAG,SAAS;IAI/D;;;OAGG;IACI,SAAS,IAAI,OAAO;IAI3B;;;OAGG;IACI,OAAO,IAAI,MAAM;IAOxB;;OAEG;IACI,WAAW,IAAI,MAAM,GAAG,SAAS;CAGzC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE1D;;OAEG;IACH,iBAAiB,CAAC,EAAE,wBAAwB,CAAC;IAE7C;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAEjC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE;QACb,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAED;;;GAGG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAC,CAAkC;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAgB;IACxC,OAAO,CAAC,eAAe,CAAC,CAAa;gBAElB,OAAO,EAAE,aAAa;IAIzC;;OAEG;IACI,KAAK,IAAI,IAAI;IAkJpB;;OAEG;IACI,IAAI,IAAI,IAAI;IAWnB;;;;;OAKG;IACU,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2C9D;;OAEG;IACI,iBAAiB,IAAI,MAAM;IAIlC;;OAEG;IACI,mBAAmB,IAAI,OAAO;IAIrC;;;OAGG;IACI,SAAS,IAAI,MAAM,CAAC,uBAAuB,CAAC,GAAG,SAAS;IAI/D;;;OAGG;IACI,SAAS,IAAI,OAAO;IAI3B;;;OAGG;IACI,OAAO,IAAI,MAAM;IAOxB;;OAEG;IACI,WAAW,IAAI,MAAM,GAAG,SAAS;IAIxC;;;;;;;OAOG;IACH,OAAO,CAAC,MAAM,CAAC,oBAAoB;CAyDpC"}
package/dist/index.js CHANGED
@@ -4154,6 +4154,7 @@ class Context {
4154
4154
  files = [];
4155
4155
  _body;
4156
4156
  _bodyParsed = false;
4157
+ signal;
4157
4158
  constructor(request) {
4158
4159
  this.request = request;
4159
4160
  this.url = new URL2(request.url);
@@ -4162,6 +4163,7 @@ class Context {
4162
4163
  this.query = this.url.searchParams;
4163
4164
  this.headers = request.headers;
4164
4165
  this.responseHeaders = new Headers;
4166
+ this.signal = request.signal;
4165
4167
  }
4166
4168
  async getBody() {
4167
4169
  if (!this._bodyParsed) {
@@ -4307,7 +4309,27 @@ class BunServer {
4307
4309
  this.isShuttingDown = false;
4308
4310
  this.shutdownPromise = undefined;
4309
4311
  this.shutdownResolve = undefined;
4310
- const fetchHandler = (request, server) => {
4312
+ const sseKeepAlive = this.options.sseKeepAlive;
4313
+ const sseHeartbeatEnabled = sseKeepAlive?.enabled !== false;
4314
+ const sseHeartbeatIntervalMs = sseKeepAlive?.intervalMs ?? 15000;
4315
+ const postProcessSse = (response, request, bunServer) => {
4316
+ const ct = response.headers.get("content-type");
4317
+ if (!ct?.includes("text/event-stream")) {
4318
+ return response;
4319
+ }
4320
+ bunServer.timeout(request, 0);
4321
+ if (sseHeartbeatEnabled && response.body) {
4322
+ return BunServer.wrapSseWithHeartbeat(response, sseHeartbeatIntervalMs, request.signal);
4323
+ }
4324
+ return response;
4325
+ };
4326
+ const decrementAndMaybeShutdown = () => {
4327
+ this.activeRequests--;
4328
+ if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
4329
+ this.shutdownResolve();
4330
+ }
4331
+ };
4332
+ const fetchHandler = (request, bunServer) => {
4311
4333
  if (this.isShuttingDown) {
4312
4334
  return new Response("Server is shutting down", { status: 503 });
4313
4335
  }
@@ -4319,7 +4341,7 @@ class BunServer {
4319
4341
  }
4320
4342
  const context2 = new Context(request);
4321
4343
  const queryParams = new URLSearchParams(url.searchParams);
4322
- const upgraded = server.upgrade(request, {
4344
+ const upgraded = bunServer.upgrade(request, {
4323
4345
  data: {
4324
4346
  path: url.pathname,
4325
4347
  query: queryParams,
@@ -4335,19 +4357,12 @@ class BunServer {
4335
4357
  const context = new Context(request);
4336
4358
  const responsePromise = this.options.fetch(context);
4337
4359
  if (responsePromise instanceof Promise) {
4338
- responsePromise.finally(() => {
4339
- this.activeRequests--;
4340
- if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
4341
- this.shutdownResolve();
4342
- }
4343
- }).catch(() => {});
4344
- } else {
4345
- this.activeRequests--;
4346
- if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
4347
- this.shutdownResolve();
4348
- }
4360
+ const processed = responsePromise.then((response) => postProcessSse(response, request, bunServer));
4361
+ processed.finally(decrementAndMaybeShutdown).catch(() => {});
4362
+ return processed;
4349
4363
  }
4350
- return responsePromise;
4364
+ decrementAndMaybeShutdown();
4365
+ return postProcessSse(responsePromise, request, bunServer);
4351
4366
  };
4352
4367
  const websocketHandlers = {
4353
4368
  open: async (ws) => {
@@ -4445,6 +4460,68 @@ class BunServer {
4445
4460
  getHostname() {
4446
4461
  return this.options.hostname;
4447
4462
  }
4463
+ static wrapSseWithHeartbeat(original, intervalMs, signal) {
4464
+ const encoder = new TextEncoder;
4465
+ const keepaliveChunk = encoder.encode(`: keepalive
4466
+
4467
+ `);
4468
+ const originalBody = original.body;
4469
+ let heartbeat;
4470
+ let reader;
4471
+ const wrapped = new ReadableStream({
4472
+ start(controller) {
4473
+ reader = originalBody.getReader();
4474
+ heartbeat = setInterval(() => {
4475
+ try {
4476
+ controller.enqueue(keepaliveChunk);
4477
+ } catch {
4478
+ clearInterval(heartbeat);
4479
+ heartbeat = undefined;
4480
+ }
4481
+ }, intervalMs);
4482
+ const onAbort = () => {
4483
+ if (heartbeat) {
4484
+ clearInterval(heartbeat);
4485
+ heartbeat = undefined;
4486
+ }
4487
+ };
4488
+ signal.addEventListener("abort", onAbort, { once: true });
4489
+ const pump = async () => {
4490
+ try {
4491
+ while (true) {
4492
+ const { done, value } = await reader.read();
4493
+ if (done)
4494
+ break;
4495
+ controller.enqueue(value);
4496
+ }
4497
+ controller.close();
4498
+ } catch (err) {
4499
+ try {
4500
+ controller.error(err);
4501
+ } catch {}
4502
+ } finally {
4503
+ if (heartbeat) {
4504
+ clearInterval(heartbeat);
4505
+ heartbeat = undefined;
4506
+ }
4507
+ signal.removeEventListener("abort", onAbort);
4508
+ }
4509
+ };
4510
+ pump();
4511
+ },
4512
+ cancel() {
4513
+ if (heartbeat) {
4514
+ clearInterval(heartbeat);
4515
+ heartbeat = undefined;
4516
+ }
4517
+ reader?.cancel();
4518
+ }
4519
+ });
4520
+ return new Response(wrapped, {
4521
+ status: original.status,
4522
+ headers: original.headers
4523
+ });
4524
+ }
4448
4525
  }
4449
4526
 
4450
4527
  // src/core/application.ts
@@ -6675,6 +6752,7 @@ class Application {
6675
6752
  hostname: finalHostname,
6676
6753
  reusePort: this.options.reusePort,
6677
6754
  idleTimeout: this.options.idleTimeout,
6755
+ sseKeepAlive: this.options.sseKeepAlive,
6678
6756
  fetch: this.handleRequest.bind(this),
6679
6757
  websocketRegistry: this.websocketRegistry,
6680
6758
  gracefulShutdownTimeout: this.options.gracefulShutdownTimeout
@@ -13982,7 +14060,7 @@ class AiService {
13982
14060
  const fallback = this.options.fallback ?? false;
13983
14061
  const timeout = this.options.timeout ?? 30000;
13984
14062
  if (!fallback) {
13985
- return this.withTimeout(this.getProvider(targetName).complete(request), timeout, targetName);
14063
+ return this.withTimeout(this.getProvider(targetName).complete(request), timeout, targetName, request.signal);
13986
14064
  }
13987
14065
  const names = [
13988
14066
  targetName,
@@ -13994,21 +14072,32 @@ class AiService {
13994
14072
  const provider = this.providers.get(name);
13995
14073
  if (!provider)
13996
14074
  continue;
13997
- return await this.withTimeout(provider.complete({ ...request, provider: name }), timeout, name);
14075
+ return await this.withTimeout(provider.complete({ ...request, provider: name }), timeout, name, request.signal);
13998
14076
  } catch (err) {
13999
14077
  errors.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
14000
14078
  }
14001
14079
  }
14002
14080
  throw new AiAllProvidersFailed(errors);
14003
14081
  }
14004
- withTimeout(promise, ms, providerName) {
14082
+ withTimeout(promise, ms, providerName, signal) {
14005
14083
  return new Promise((resolve2, reject) => {
14084
+ if (signal?.aborted) {
14085
+ reject(signal.reason ?? new Error("Aborted"));
14086
+ return;
14087
+ }
14006
14088
  const timer = setTimeout(() => reject(new AiTimeoutError(providerName, ms)), ms);
14089
+ const onAbort = () => {
14090
+ clearTimeout(timer);
14091
+ reject(signal.reason ?? new Error("Aborted"));
14092
+ };
14093
+ signal?.addEventListener("abort", onAbort, { once: true });
14007
14094
  promise.then((val) => {
14008
14095
  clearTimeout(timer);
14096
+ signal?.removeEventListener("abort", onAbort);
14009
14097
  resolve2(val);
14010
14098
  }, (err) => {
14011
14099
  clearTimeout(timer);
14100
+ signal?.removeEventListener("abort", onAbort);
14012
14101
  reject(err);
14013
14102
  });
14014
14103
  });
@@ -14134,7 +14223,7 @@ class OpenAIProvider {
14134
14223
  function: { name: t.name, description: t.description, parameters: t.parameters }
14135
14224
  }));
14136
14225
  }
14137
- const response = await this.post("/chat/completions", body);
14226
+ const response = await this.post("/chat/completions", body, request.signal);
14138
14227
  const choice = response.choices?.[0];
14139
14228
  const usage = response.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
14140
14229
  const message = choice?.message;
@@ -14174,6 +14263,7 @@ class OpenAIProvider {
14174
14263
  const encoder = new TextEncoder;
14175
14264
  const apiKey = this.apiKey;
14176
14265
  const baseUrl = this.baseUrl;
14266
+ const signal = request.signal;
14177
14267
  return new ReadableStream({
14178
14268
  async start(controller2) {
14179
14269
  try {
@@ -14183,7 +14273,8 @@ class OpenAIProvider {
14183
14273
  "Content-Type": "application/json",
14184
14274
  Authorization: `Bearer ${apiKey}`
14185
14275
  },
14186
- body: JSON.stringify(body)
14276
+ body: JSON.stringify(body),
14277
+ signal
14187
14278
  });
14188
14279
  if (!res.ok || !res.body) {
14189
14280
  const err = await res.text();
@@ -14237,14 +14328,15 @@ class OpenAIProvider {
14237
14328
  countTokens(messages) {
14238
14329
  return Math.ceil(messages.reduce((sum, m) => sum + m.content.length, 0) / 4);
14239
14330
  }
14240
- async post(path, body) {
14331
+ async post(path, body, signal) {
14241
14332
  const res = await fetch(`${this.baseUrl}${path}`, {
14242
14333
  method: "POST",
14243
14334
  headers: {
14244
14335
  "Content-Type": "application/json",
14245
14336
  Authorization: `Bearer ${this.apiKey}`
14246
14337
  },
14247
- body: JSON.stringify(body)
14338
+ body: JSON.stringify(body),
14339
+ signal
14248
14340
  });
14249
14341
  if (res.status === 429) {
14250
14342
  const retryAfter = res.headers.get("retry-after");
@@ -14320,7 +14412,7 @@ class AnthropicProvider {
14320
14412
  input_schema: t.parameters
14321
14413
  }));
14322
14414
  }
14323
- const response = await this.post("/v1/messages", body);
14415
+ const response = await this.post("/v1/messages", body, request.signal);
14324
14416
  const usage = response["usage"] ?? { input_tokens: 0, output_tokens: 0 };
14325
14417
  let content = "";
14326
14418
  const toolCalls = [];
@@ -14368,6 +14460,7 @@ class AnthropicProvider {
14368
14460
  const baseUrl = this.baseUrl;
14369
14461
  const anthropicVersion = this.anthropicVersion;
14370
14462
  const encoder = new TextEncoder;
14463
+ const signal = request.signal;
14371
14464
  return new ReadableStream({
14372
14465
  async start(controller2) {
14373
14466
  try {
@@ -14378,7 +14471,8 @@ class AnthropicProvider {
14378
14471
  "x-api-key": apiKey,
14379
14472
  "anthropic-version": anthropicVersion
14380
14473
  },
14381
- body: JSON.stringify(body)
14474
+ body: JSON.stringify(body),
14475
+ signal
14382
14476
  });
14383
14477
  if (!res.ok || !res.body) {
14384
14478
  controller2.enqueue(encoder.encode(`data: ${JSON.stringify({ error: await res.text(), done: true })}
@@ -14428,7 +14522,7 @@ class AnthropicProvider {
14428
14522
  countTokens(messages) {
14429
14523
  return Math.ceil(messages.reduce((sum, m) => sum + m.content.length, 0) / 4);
14430
14524
  }
14431
- async post(path, body) {
14525
+ async post(path, body, signal) {
14432
14526
  const res = await fetch(`${this.baseUrl}${path}`, {
14433
14527
  method: "POST",
14434
14528
  headers: {
@@ -14436,7 +14530,8 @@ class AnthropicProvider {
14436
14530
  "x-api-key": this.apiKey,
14437
14531
  "anthropic-version": this.anthropicVersion
14438
14532
  },
14439
- body: JSON.stringify(body)
14533
+ body: JSON.stringify(body),
14534
+ signal
14440
14535
  });
14441
14536
  if (res.status === 429)
14442
14537
  throw new AiRateLimitError(this.name);
@@ -14469,7 +14564,8 @@ class OllamaProvider {
14469
14564
  temperature: request.temperature,
14470
14565
  num_predict: request.maxTokens
14471
14566
  }
14472
- })
14567
+ }),
14568
+ signal: request.signal
14473
14569
  });
14474
14570
  if (!res.ok) {
14475
14571
  throw new AiProviderError(await res.text(), this.name, res.status);
@@ -14494,6 +14590,7 @@ class OllamaProvider {
14494
14590
  const model = request.model ?? this.defaultModel;
14495
14591
  const baseUrl = this.baseUrl;
14496
14592
  const encoder = new TextEncoder;
14593
+ const signal = request.signal;
14497
14594
  return new ReadableStream({
14498
14595
  async start(controller2) {
14499
14596
  try {
@@ -14508,7 +14605,8 @@ class OllamaProvider {
14508
14605
  temperature: request.temperature,
14509
14606
  num_predict: request.maxTokens
14510
14607
  }
14511
- })
14608
+ }),
14609
+ signal
14512
14610
  });
14513
14611
  if (!res.ok || !res.body) {
14514
14612
  controller2.enqueue(encoder.encode(`data: ${JSON.stringify({ error: await res.text(), done: true })}
@@ -14590,7 +14688,8 @@ class GoogleProvider {
14590
14688
  const res = await fetch(`${this.baseUrl}/models/${model}:generateContent?key=${this.apiKey}`, {
14591
14689
  method: "POST",
14592
14690
  headers: { "Content-Type": "application/json" },
14593
- body: JSON.stringify(body)
14691
+ body: JSON.stringify(body),
14692
+ signal: request.signal
14594
14693
  });
14595
14694
  if (res.status === 429)
14596
14695
  throw new AiRateLimitError(this.name);
@@ -14633,6 +14732,7 @@ class GoogleProvider {
14633
14732
  const apiKey = this.apiKey;
14634
14733
  const baseUrl = this.baseUrl;
14635
14734
  const encoder = new TextEncoder;
14735
+ const signal = request.signal;
14636
14736
  const body = {
14637
14737
  contents,
14638
14738
  generationConfig: { temperature: request.temperature, maxOutputTokens: request.maxTokens }
@@ -14645,7 +14745,8 @@ class GoogleProvider {
14645
14745
  const res = await fetch(`${baseUrl}/models/${model}:streamGenerateContent?key=${apiKey}&alt=sse`, {
14646
14746
  method: "POST",
14647
14747
  headers: { "Content-Type": "application/json" },
14648
- body: JSON.stringify(body)
14748
+ body: JSON.stringify(body),
14749
+ signal
14649
14750
  });
14650
14751
  if (!res.ok || !res.body) {
14651
14752
  controller2.enqueue(encoder.encode(`data: ${JSON.stringify({ error: await res.text(), done: true })}
@@ -16009,8 +16110,6 @@ class McpServer {
16009
16110
  });
16010
16111
  }
16011
16112
  createSseResponse() {
16012
- const registry = this.registry;
16013
- const serverInfo = this.serverInfo;
16014
16113
  const encoder = new TextEncoder;
16015
16114
  const stream = new ReadableStream({
16016
16115
  start(controller2) {
@@ -16022,15 +16121,6 @@ data: ${JSON.stringify({
16022
16121
 
16023
16122
  `;
16024
16123
  controller2.enqueue(encoder.encode(initEvent));
16025
- const pingInterval = setInterval(() => {
16026
- try {
16027
- controller2.enqueue(encoder.encode(`: ping
16028
-
16029
- `));
16030
- } catch (_error) {
16031
- clearInterval(pingInterval);
16032
- }
16033
- }, 15000);
16034
16124
  }
16035
16125
  });
16036
16126
  return new Response(stream, {
@@ -16038,7 +16128,7 @@ data: ${JSON.stringify({
16038
16128
  "Content-Type": "text/event-stream",
16039
16129
  "Cache-Control": "no-cache",
16040
16130
  Connection: "keep-alive",
16041
- "X-MCP-Server": `${serverInfo.name}/${serverInfo.version}`
16131
+ "X-MCP-Server": `${this.serverInfo.name}/${this.serverInfo.version}`
16042
16132
  }
16043
16133
  });
16044
16134
  }
@@ -20,8 +20,11 @@ export declare class McpServer {
20
20
  */
21
21
  handleHttp(req: Request): Promise<Response>;
22
22
  /**
23
- * Create an SSE response that keeps the connection open for streaming
24
- * This is the SSE transport endpoint
23
+ * Create an SSE response that keeps the connection open for streaming.
24
+ *
25
+ * Heartbeat / keep-alive pings are handled automatically by the framework's
26
+ * SSE post-processor (`sseKeepAlive`), so this method no longer injects its
27
+ * own `setInterval`.
25
28
  */
26
29
  createSseResponse(): Response;
27
30
  private dispatch;
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;gBAExB,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa;IAKnE;;OAEG;IACU,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAgBtE;;;OAGG;IACU,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQxD;;;OAGG;IACI,iBAAiB,IAAI,QAAQ;YAmCtB,QAAQ;CAkEvB"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;GAKG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAc;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;gBAExB,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa;IAKnE;;OAEG;IACU,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAgBtE;;;OAGG;IACU,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQxD;;;;;;OAMG;IACI,iBAAiB,IAAI,QAAQ;YAuBtB,QAAQ;CAkEvB"}
@@ -1,22 +1,33 @@
1
- # idleTimeout
1
+ # idleTimeout & SSE Keep-Alive
2
2
 
3
- `idleTimeout` now supports both global and per-route configuration.
3
+ ## Two layers of timeout
4
+
5
+ Bun Server has **two independent timeout mechanisms** — understanding their difference is critical for SSE / streaming use cases.
6
+
7
+ | Layer | Config | Scope | Mechanism |
8
+ |-------|--------|-------|-----------|
9
+ | **TCP connection** | `Application({ idleTimeout })` | Bun.serve level | Bun kernel closes the TCP socket when no bytes flow for N seconds |
10
+ | **Handler logic** | `@IdleTimeout(ms)` decorator | Per-route `Promise.race` | Returns `408 Request Timeout` if the handler doesn't resolve in time |
11
+
12
+ > **Key point:** For SSE responses the handler returns a `Response` immediately (with a streaming body). The handler-level `@IdleTimeout` has already resolved at that point and will **not** protect or kill the stream. Only the TCP-level `idleTimeout` can break an SSE connection.
13
+
14
+ ---
4
15
 
5
16
  ## Global idle timeout (milliseconds)
6
17
 
7
- Set in `Application` options using milliseconds.
8
- Framework converts internally before passing to `Bun.serve`.
18
+ Set in `Application` options using milliseconds.
19
+ Framework converts internally before passing to `Bun.serve` (`Math.ceil(ms / 1000)` → seconds).
9
20
 
10
21
  ```ts
11
22
  const app = new Application({
12
23
  port: 3000,
13
- idleTimeout: 15000, // ms
24
+ idleTimeout: 15000, // 15 s — applies to all non-SSE connections
14
25
  });
15
26
  ```
16
27
 
17
- ## Per-route timeout (milliseconds)
28
+ ## Per-route timeout — `@IdleTimeout(ms)`
18
29
 
19
- Use `@IdleTimeout(ms)` on controller class or handler method.
30
+ Use `@IdleTimeout(ms)` on a controller class or a handler method.
20
31
 
21
32
  ```ts
22
33
  import { Controller, GET, IdleTimeout } from '@dangao/bun-server';
@@ -38,5 +49,85 @@ class ApiController {
38
49
  }
39
50
  ```
40
51
 
41
- When timeout is reached, Bun Server throws `HttpException(408, "Request Timeout")`.
52
+ ### Matching & precedence
53
+
54
+ 1. **Method-level** `@IdleTimeout` is checked first — if present, it wins.
55
+ 2. **Class-level** `@IdleTimeout` is used as fallback.
56
+ 3. If neither is set, no handler-level timeout is applied (the route runs until the TCP timeout or until it completes).
57
+
58
+ When the handler-level timeout fires, the framework throws `HttpException(408, "Request Timeout")`.
59
+
60
+ ---
61
+
62
+ ## SSE Keep-Alive (automatic)
63
+
64
+ When the framework detects a response with `Content-Type: text/event-stream`, it automatically:
65
+
66
+ 1. **Disables the TCP idle timeout** for that request via `server.timeout(req, 0)`, preventing Bun from killing the long-lived connection.
67
+ 2. **Injects SSE comment heartbeats** (`: keepalive\n\n`) at a configurable interval, preventing intermediate proxies (nginx, cloud load balancers) from closing the connection due to inactivity.
68
+
69
+ ### Configuration
70
+
71
+ ```ts
72
+ const app = new Application({
73
+ port: 3000,
74
+ idleTimeout: 10000, // normal requests: 10 s
75
+
76
+ // SSE keep-alive — defaults shown below
77
+ sseKeepAlive: {
78
+ enabled: true, // auto-detect SSE and inject heartbeat
79
+ intervalMs: 15000, // heartbeat every 15 s
80
+ },
81
+ });
82
+ ```
83
+
84
+ | Option | Type | Default | Description |
85
+ |--------|------|---------|-------------|
86
+ | `sseKeepAlive.enabled` | `boolean` | `true` | Enable automatic SSE detection, TCP timeout reset, and heartbeat injection |
87
+ | `sseKeepAlive.intervalMs` | `number` | `15000` | Heartbeat interval in milliseconds |
88
+
89
+ When `enabled` is `true` (default), any response whose `Content-Type` header contains `text/event-stream` triggers the SSE post-processor. You do **not** need any special decorator or annotation — detection is purely based on the response header.
90
+
91
+ When `enabled` is `false`, no SSE-specific processing is applied. You would need to manage keep-alive and `server.timeout` yourself.
92
+
93
+ ---
94
+
95
+ ## Signal cascading (`ctx.signal`)
96
+
97
+ `Context` exposes the client's `AbortSignal` via `ctx.signal`. When the client disconnects (network failure, browser tab closed, `curl` interrupted), this signal aborts.
98
+
99
+ For AI streaming endpoints, pass `ctx.signal` to `AiService` so the upstream API request is cancelled immediately — **stopping token consumption**:
100
+
101
+ ```ts
102
+ import { Controller, GET, Context as Ctx } from '@dangao/bun-server';
103
+ import type { Context } from '@dangao/bun-server';
104
+
105
+ @Controller('/chat')
106
+ class ChatController {
107
+ constructor(private readonly ai: AiService) {}
108
+
109
+ @GET('/stream')
110
+ public stream(@Ctx() ctx: Context) {
111
+ const stream = this.ai.stream({
112
+ messages: [{ role: 'user', content: 'Hello' }],
113
+ signal: ctx.signal, // ← cascade client disconnect
114
+ });
115
+ return new Response(stream, {
116
+ headers: { 'Content-Type': 'text/event-stream' },
117
+ });
118
+ }
119
+ }
120
+ ```
121
+
122
+ > **Note:** The `Context` parameter decorator is exported as both `Context` (from `'./controller'`) and `ContextParam` (from the package root). Use whichever alias avoids name collision with the `Context` type.
123
+
124
+ The full cancellation chain:
42
125
 
126
+ ```
127
+ Client disconnects
128
+ → request.signal aborts
129
+ → heartbeat timer cleared
130
+ → wrapped stream cancelled
131
+ → AI provider fetch() aborted
132
+ → upstream API connection closed (tokens saved)
133
+ ```
@@ -1,19 +1,30 @@
1
- # idleTimeout
1
+ # idleTimeout 与 SSE 保活
2
2
 
3
- `idleTimeout` 现已支持全局与路由级两种配置方式。
3
+ ## 两层超时机制
4
+
5
+ Bun Server 有**两套独立的超时机制**——理解它们的区别对 SSE / 流式场景至关重要。
6
+
7
+ | 层面 | 配置方式 | 作用域 | 机制 |
8
+ |------|----------|--------|------|
9
+ | **TCP 连接级** | `Application({ idleTimeout })` | Bun.serve 底层 | Bun 内核在连接无数据流动 N 秒后直接断开 socket |
10
+ | **Handler 逻辑级** | `@IdleTimeout(ms)` 装饰器 | 路由粒度的 `Promise.race` | handler 未在指定时间内 resolve 则返回 `408 Request Timeout` |
11
+
12
+ > **关键:** 对于 SSE 响应,handler 会立即返回一个带流式 body 的 `Response`。此时 handler 层面的 `@IdleTimeout` 已经 resolve,**不会**保护或终止该流。只有 TCP 级别的 `idleTimeout` 才能断开 SSE 连接。
13
+
14
+ ---
4
15
 
5
16
  ## 全局 idleTimeout(毫秒)
6
17
 
7
- 在 `Application` 中直接按毫秒设置,框架内部会转换后传给 `Bun.serve`。
18
+ 在 `Application` 中按毫秒设置,框架内部自动转换后传给 `Bun.serve`(`Math.ceil(ms / 1000)` → 秒)。
8
19
 
9
20
  ```ts
10
21
  const app = new Application({
11
22
  port: 3000,
12
- idleTimeout: 15000, // ms
23
+ idleTimeout: 15000, // 15 秒 — 对所有非 SSE 连接生效
13
24
  });
14
25
  ```
15
26
 
16
- ## 路由级超时(毫秒)
27
+ ## 路由级超时 — `@IdleTimeout(ms)`
17
28
 
18
29
  使用 `@IdleTimeout(ms)` 装饰器配置控制器级或方法级超时。
19
30
 
@@ -37,5 +48,85 @@ class ApiController {
37
48
  }
38
49
  ```
39
50
 
40
- 超时后会抛出 `HttpException(408, "Request Timeout")`。
51
+ ### 匹配与生效规则
52
+
53
+ 1. **方法级** `@IdleTimeout` 优先检测——存在即生效。
54
+ 2. **类级** `@IdleTimeout` 作为兜底。
55
+ 3. 若均未设置,则不应用 handler 级超时(路由将持续运行直到 TCP 超时或自然完成)。
56
+
57
+ handler 级超时触发时,框架抛出 `HttpException(408, "Request Timeout")`。
58
+
59
+ ---
60
+
61
+ ## SSE 保活(自动)
62
+
63
+ 当框架检测到响应的 `Content-Type` 包含 `text/event-stream` 时,会自动执行:
64
+
65
+ 1. **禁用该请求的 TCP 空闲超时** —— 通过 `server.timeout(req, 0)` 阻止 Bun 断开长连接。
66
+ 2. **注入 SSE 注释心跳** —— 按配置间隔发送 `: keepalive\n\n`,防止中间代理(nginx、云 LB)因空闲而断连。
67
+
68
+ ### 配置
69
+
70
+ ```ts
71
+ const app = new Application({
72
+ port: 3000,
73
+ idleTimeout: 10000, // 普通请求:10 秒
74
+
75
+ // SSE 保活 — 以下为默认值
76
+ sseKeepAlive: {
77
+ enabled: true, // 自动检测 SSE 并注入心跳
78
+ intervalMs: 15000, // 每 15 秒一次心跳
79
+ },
80
+ });
81
+ ```
82
+
83
+ | 选项 | 类型 | 默认值 | 说明 |
84
+ |------|------|--------|------|
85
+ | `sseKeepAlive.enabled` | `boolean` | `true` | 启用 SSE 自动检测、TCP 超时解除和心跳注入 |
86
+ | `sseKeepAlive.intervalMs` | `number` | `15000` | 心跳间隔(毫秒) |
87
+
88
+ 当 `enabled` 为 `true`(默认)时,任何 `Content-Type` 头包含 `text/event-stream` 的响应都会触发 SSE 后处理器。**无需任何特殊装饰器或注解**——纯粹基于响应头自动检测。
89
+
90
+ 当 `enabled` 为 `false` 时,框架不做任何 SSE 特殊处理。你需要自行管理 keep-alive 和 `server.timeout`。
91
+
92
+ ---
93
+
94
+ ## 信号级联(`ctx.signal`)
95
+
96
+ `Context` 通过 `ctx.signal` 暴露客户端的 `AbortSignal`。当客户端断连(网络故障、关闭浏览器标签、中断 `curl`)时,该信号会 abort。
97
+
98
+ 对于 AI 流式端点,将 `ctx.signal` 传递给 `AiService`,可立即取消上游 API 请求——**停止 token 消耗**:
99
+
100
+ ```ts
101
+ import { Controller, GET, Context as Ctx } from '@dangao/bun-server';
102
+ import type { Context } from '@dangao/bun-server';
103
+
104
+ @Controller('/chat')
105
+ class ChatController {
106
+ constructor(private readonly ai: AiService) {}
107
+
108
+ @GET('/stream')
109
+ public stream(@Ctx() ctx: Context) {
110
+ const stream = this.ai.stream({
111
+ messages: [{ role: 'user', content: 'Hello' }],
112
+ signal: ctx.signal, // ← 级联客户端断连
113
+ });
114
+ return new Response(stream, {
115
+ headers: { 'Content-Type': 'text/event-stream' },
116
+ });
117
+ }
118
+ }
119
+ ```
120
+
121
+ > **注意:** `Context` 参数装饰器既可通过 `Context`(从 `'./controller'`)导入,也可通过包根导出的别名 `ContextParam` 导入。推荐使用 `ContextParam` 或 `Context as Ctx` 以避免与 `Context` 类型名冲突。
122
+
123
+ 完整取消链路:
41
124
 
125
+ ```
126
+ 客户端断连
127
+ → request.signal abort
128
+ → 心跳定时器清理
129
+ → 包裹流取消
130
+ → AI Provider 内部 fetch() abort
131
+ → 上游 API 连接关闭(节省 token)
132
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dangao/bun-server",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -56,7 +56,7 @@ export class AnthropicProvider implements LlmProvider {
56
56
  }));
57
57
  }
58
58
 
59
- const response = await this.post('/v1/messages', body);
59
+ const response = await this.post('/v1/messages', body, request.signal);
60
60
  const usage = (response['usage'] as { input_tokens: number; output_tokens: number }) ?? { input_tokens: 0, output_tokens: 0 };
61
61
 
62
62
  let content = '';
@@ -108,6 +108,7 @@ export class AnthropicProvider implements LlmProvider {
108
108
  const baseUrl = this.baseUrl;
109
109
  const anthropicVersion = this.anthropicVersion;
110
110
  const encoder = new TextEncoder();
111
+ const signal = request.signal;
111
112
 
112
113
  return new ReadableStream<Uint8Array>({
113
114
  async start(controller) {
@@ -120,6 +121,7 @@ export class AnthropicProvider implements LlmProvider {
120
121
  'anthropic-version': anthropicVersion,
121
122
  },
122
123
  body: JSON.stringify(body),
124
+ signal,
123
125
  });
124
126
 
125
127
  if (!res.ok || !res.body) {
@@ -170,7 +172,7 @@ export class AnthropicProvider implements LlmProvider {
170
172
  return Math.ceil(messages.reduce((sum, m) => sum + m.content.length, 0) / 4);
171
173
  }
172
174
 
173
- private async post(path: string, body: Record<string, unknown>): Promise<Record<string, unknown>> {
175
+ private async post(path: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<Record<string, unknown>> {
174
176
  const res = await fetch(`${this.baseUrl}${path}`, {
175
177
  method: 'POST',
176
178
  headers: {
@@ -179,6 +181,7 @@ export class AnthropicProvider implements LlmProvider {
179
181
  'anthropic-version': this.anthropicVersion,
180
182
  },
181
183
  body: JSON.stringify(body),
184
+ signal,
182
185
  });
183
186
 
184
187
  if (res.status === 429) throw new AiRateLimitError(this.name);
@@ -50,6 +50,7 @@ export class GoogleProvider implements LlmProvider {
50
50
  method: 'POST',
51
51
  headers: { 'Content-Type': 'application/json' },
52
52
  body: JSON.stringify(body),
53
+ signal: request.signal,
53
54
  },
54
55
  );
55
56
 
@@ -95,6 +96,7 @@ export class GoogleProvider implements LlmProvider {
95
96
  const apiKey = this.apiKey;
96
97
  const baseUrl = this.baseUrl;
97
98
  const encoder = new TextEncoder();
99
+ const signal = request.signal;
98
100
 
99
101
  const body: Record<string, unknown> = {
100
102
  contents,
@@ -111,6 +113,7 @@ export class GoogleProvider implements LlmProvider {
111
113
  method: 'POST',
112
114
  headers: { 'Content-Type': 'application/json' },
113
115
  body: JSON.stringify(body),
116
+ signal,
114
117
  },
115
118
  );
116
119
 
@@ -33,6 +33,7 @@ export class OllamaProvider implements LlmProvider {
33
33
  num_predict: request.maxTokens,
34
34
  },
35
35
  }),
36
+ signal: request.signal,
36
37
  });
37
38
 
38
39
  if (!res.ok) {
@@ -61,6 +62,7 @@ export class OllamaProvider implements LlmProvider {
61
62
  const model = request.model ?? this.defaultModel;
62
63
  const baseUrl = this.baseUrl;
63
64
  const encoder = new TextEncoder();
65
+ const signal = request.signal;
64
66
 
65
67
  return new ReadableStream<Uint8Array>({
66
68
  async start(controller) {
@@ -77,6 +79,7 @@ export class OllamaProvider implements LlmProvider {
77
79
  num_predict: request.maxTokens,
78
80
  },
79
81
  }),
82
+ signal,
80
83
  });
81
84
 
82
85
  if (!res.ok || !res.body) {
@@ -79,7 +79,7 @@ export class OpenAIProvider implements LlmProvider {
79
79
  }));
80
80
  }
81
81
 
82
- const response = await this.post('/chat/completions', body);
82
+ const response = await this.post('/chat/completions', body, request.signal);
83
83
  const choice = response.choices?.[0];
84
84
  const usage = response.usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
85
85
  const message = choice?.message;
@@ -127,6 +127,7 @@ export class OpenAIProvider implements LlmProvider {
127
127
  const encoder = new TextEncoder();
128
128
  const apiKey = this.apiKey;
129
129
  const baseUrl = this.baseUrl;
130
+ const signal = request.signal;
130
131
 
131
132
  return new ReadableStream<Uint8Array>({
132
133
  async start(controller) {
@@ -138,6 +139,7 @@ export class OpenAIProvider implements LlmProvider {
138
139
  'Authorization': `Bearer ${apiKey}`,
139
140
  },
140
141
  body: JSON.stringify(body),
142
+ signal,
141
143
  });
142
144
 
143
145
  if (!res.ok || !res.body) {
@@ -191,7 +193,7 @@ export class OpenAIProvider implements LlmProvider {
191
193
  return Math.ceil(messages.reduce((sum, m) => sum + m.content.length, 0) / 4);
192
194
  }
193
195
 
194
- private async post(path: string, body: Record<string, unknown>): Promise<OpenAiChatCompletionResponse> {
196
+ private async post(path: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<OpenAiChatCompletionResponse> {
195
197
  const res = await fetch(`${this.baseUrl}${path}`, {
196
198
  method: 'POST',
197
199
  headers: {
@@ -199,6 +201,7 @@ export class OpenAIProvider implements LlmProvider {
199
201
  'Authorization': `Bearer ${this.apiKey}`,
200
202
  },
201
203
  body: JSON.stringify(body),
204
+ signal,
202
205
  });
203
206
 
204
207
  if (res.status === 429) {
package/src/ai/service.ts CHANGED
@@ -120,7 +120,7 @@ export class AiService {
120
120
  const timeout = this.options.timeout ?? 30000;
121
121
 
122
122
  if (!fallback) {
123
- return this.withTimeout(this.getProvider(targetName).complete(request), timeout, targetName);
123
+ return this.withTimeout(this.getProvider(targetName).complete(request), timeout, targetName, request.signal);
124
124
  }
125
125
 
126
126
  // Fallback chain: try target first, then others in order
@@ -134,7 +134,7 @@ export class AiService {
134
134
  try {
135
135
  const provider = this.providers.get(name);
136
136
  if (!provider) continue;
137
- return await this.withTimeout(provider.complete({ ...request, provider: name }), timeout, name);
137
+ return await this.withTimeout(provider.complete({ ...request, provider: name }), timeout, name, request.signal);
138
138
  } catch (err) {
139
139
  errors.push(`${name}: ${err instanceof Error ? err.message : String(err)}`);
140
140
  }
@@ -143,12 +143,24 @@ export class AiService {
143
143
  throw new AiAllProvidersFailed(errors);
144
144
  }
145
145
 
146
- private withTimeout<T>(promise: Promise<T>, ms: number, providerName: string): Promise<T> {
146
+ private withTimeout<T>(promise: Promise<T>, ms: number, providerName: string, signal?: AbortSignal): Promise<T> {
147
147
  return new Promise<T>((resolve, reject) => {
148
+ if (signal?.aborted) {
149
+ reject(signal.reason ?? new Error('Aborted'));
150
+ return;
151
+ }
152
+
148
153
  const timer = setTimeout(() => reject(new AiTimeoutError(providerName, ms)), ms);
154
+
155
+ const onAbort = () => {
156
+ clearTimeout(timer);
157
+ reject(signal!.reason ?? new Error('Aborted'));
158
+ };
159
+ signal?.addEventListener('abort', onAbort, { once: true });
160
+
149
161
  promise.then(
150
- (val) => { clearTimeout(timer); resolve(val); },
151
- (err) => { clearTimeout(timer); reject(err); },
162
+ (val) => { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); resolve(val); },
163
+ (err) => { clearTimeout(timer); signal?.removeEventListener('abort', onAbort); reject(err); },
152
164
  );
153
165
  });
154
166
  }
package/src/ai/types.ts CHANGED
@@ -45,6 +45,11 @@ export interface AiRequest {
45
45
  tools?: AiToolDefinition[];
46
46
  /** Provider name override */
47
47
  provider?: string;
48
+ /**
49
+ * Abort signal — pass `ctx.signal` to cascade client disconnection
50
+ * to the upstream AI API, stopping token consumption immediately.
51
+ */
52
+ signal?: AbortSignal;
48
53
  }
49
54
 
50
55
  /**
@@ -61,6 +61,24 @@ export interface ApplicationOptions {
61
61
  * 框架内部会自动转换为 Bun.serve 的秒单位
62
62
  */
63
63
  idleTimeout?: number;
64
+
65
+ /**
66
+ * SSE 保活配置
67
+ *
68
+ * 框架自动检测 `Content-Type: text/event-stream` 的响应,
69
+ * 对该请求禁用 Bun TCP 空闲超时(`server.timeout(req, 0)`),
70
+ * 并按配置间隔向客户端发送 SSE 注释心跳(`: keepalive\n\n`)。
71
+ *
72
+ * 心跳可防止中间代理(nginx / 云 LB)因空闲而断开连接。
73
+ *
74
+ * @default `{ enabled: true, intervalMs: 15000 }`
75
+ */
76
+ sseKeepAlive?: {
77
+ /** 是否启用,默认 true */
78
+ enabled?: boolean;
79
+ /** 心跳间隔(毫秒),默认 15000 */
80
+ intervalMs?: number;
81
+ };
64
82
  }
65
83
 
66
84
  /**
@@ -150,6 +168,7 @@ export class Application {
150
168
  hostname: finalHostname,
151
169
  reusePort: this.options.reusePort,
152
170
  idleTimeout: this.options.idleTimeout,
171
+ sseKeepAlive: this.options.sseKeepAlive,
153
172
  fetch: this.handleRequest.bind(this),
154
173
  websocketRegistry: this.websocketRegistry,
155
174
  gracefulShutdownTimeout: this.options.gracefulShutdownTimeout,
@@ -74,6 +74,12 @@ export class Context {
74
74
  */
75
75
  private _bodyParsed: boolean = false;
76
76
 
77
+ /**
78
+ * 客户端断连信号
79
+ * 当客户端主动关闭连接时 abort,可用于级联取消内部请求
80
+ */
81
+ public readonly signal: AbortSignal;
82
+
77
83
  public constructor(request: Request) {
78
84
  this.request = request;
79
85
  this.url = new URL(request.url);
@@ -82,6 +88,7 @@ export class Context {
82
88
  this.query = this.url.searchParams;
83
89
  this.headers = request.headers;
84
90
  this.responseHeaders = new Headers();
91
+ this.signal = request.signal;
85
92
  }
86
93
 
87
94
  /**
@@ -47,6 +47,15 @@ export interface ServerOptions {
47
47
  * 框架内部会转换为 Bun.serve 所需的秒
48
48
  */
49
49
  idleTimeout?: number;
50
+
51
+ /**
52
+ * SSE 保活配置
53
+ * @see ApplicationOptions.sseKeepAlive
54
+ */
55
+ sseKeepAlive?: {
56
+ enabled?: boolean;
57
+ intervalMs?: number;
58
+ };
50
59
  }
51
60
 
52
61
  /**
@@ -81,9 +90,44 @@ export class BunServer {
81
90
  this.shutdownPromise = undefined;
82
91
  this.shutdownResolve = undefined;
83
92
 
93
+ const sseKeepAlive = this.options.sseKeepAlive;
94
+ const sseHeartbeatEnabled = sseKeepAlive?.enabled !== false;
95
+ const sseHeartbeatIntervalMs = sseKeepAlive?.intervalMs ?? 15_000;
96
+
97
+ const postProcessSse = (
98
+ response: Response,
99
+ request: Request,
100
+ bunServer: Server<WebSocketConnectionData>,
101
+ ): Response => {
102
+ const ct = response.headers.get('content-type');
103
+ if (!ct?.includes('text/event-stream')) {
104
+ return response;
105
+ }
106
+
107
+ // SSE detected — always disable Bun TCP idle timeout for this connection
108
+ bunServer.timeout(request, 0);
109
+
110
+ if (sseHeartbeatEnabled && response.body) {
111
+ return BunServer.wrapSseWithHeartbeat(
112
+ response,
113
+ sseHeartbeatIntervalMs,
114
+ request.signal,
115
+ );
116
+ }
117
+
118
+ return response;
119
+ };
120
+
121
+ const decrementAndMaybeShutdown = () => {
122
+ this.activeRequests--;
123
+ if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
124
+ this.shutdownResolve();
125
+ }
126
+ };
127
+
84
128
  const fetchHandler = (
85
129
  request: Request,
86
- server: Server<WebSocketConnectionData>,
130
+ bunServer: Server<WebSocketConnectionData>,
87
131
  ): Response | Promise<Response> | undefined => {
88
132
  if (this.isShuttingDown) {
89
133
  return new Response("Server is shutting down", { status: 503 });
@@ -101,7 +145,7 @@ export class BunServer {
101
145
  }
102
146
  const context = new Context(request);
103
147
  const queryParams = new URLSearchParams(url.searchParams);
104
- const upgraded = server.upgrade(request, {
148
+ const upgraded = bunServer.upgrade(request, {
105
149
  data: {
106
150
  path: url.pathname,
107
151
  query: queryParams,
@@ -120,24 +164,17 @@ export class BunServer {
120
164
  const responsePromise = this.options.fetch(context);
121
165
 
122
166
  if (responsePromise instanceof Promise) {
123
- responsePromise
124
- .finally(() => {
125
- this.activeRequests--;
126
- if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
127
- this.shutdownResolve();
128
- }
129
- })
130
- .catch(() => {
131
- // errors handled by middleware pipeline
132
- });
133
- } else {
134
- this.activeRequests--;
135
- if (this.isShuttingDown && this.activeRequests === 0 && this.shutdownResolve) {
136
- this.shutdownResolve();
137
- }
167
+ const processed = responsePromise.then(
168
+ (response) => postProcessSse(response, request, bunServer),
169
+ );
170
+ processed
171
+ .finally(decrementAndMaybeShutdown)
172
+ .catch(() => { /* errors handled by middleware pipeline */ });
173
+ return processed;
138
174
  }
139
175
 
140
- return responsePromise;
176
+ decrementAndMaybeShutdown();
177
+ return postProcessSse(responsePromise, request, bunServer);
141
178
  };
142
179
 
143
180
  const websocketHandlers = {
@@ -296,4 +333,70 @@ export class BunServer {
296
333
  public getHostname(): string | undefined {
297
334
  return this.options.hostname;
298
335
  }
336
+
337
+ /**
338
+ * 将 SSE Response 的 body 包裹一层心跳注入流。
339
+ *
340
+ * 原始流的数据原样透传;在数据间隙中按 intervalMs 发送
341
+ * SSE 注释帧 `: keepalive\n\n`(客户端会忽略注释帧)。
342
+ *
343
+ * 当 signal abort / 原始流结束 / 客户端断连时自动清理定时器。
344
+ */
345
+ private static wrapSseWithHeartbeat(
346
+ original: Response,
347
+ intervalMs: number,
348
+ signal: AbortSignal,
349
+ ): Response {
350
+ const encoder = new TextEncoder();
351
+ const keepaliveChunk = encoder.encode(': keepalive\n\n');
352
+ const originalBody = original.body!;
353
+ let heartbeat: ReturnType<typeof setInterval> | undefined;
354
+ let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
355
+
356
+ const wrapped = new ReadableStream<Uint8Array>({
357
+ start(controller) {
358
+ reader = originalBody.getReader() as ReadableStreamDefaultReader<Uint8Array>;
359
+
360
+ heartbeat = setInterval(() => {
361
+ try {
362
+ controller.enqueue(keepaliveChunk);
363
+ } catch {
364
+ clearInterval(heartbeat);
365
+ heartbeat = undefined;
366
+ }
367
+ }, intervalMs);
368
+
369
+ const onAbort = () => {
370
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
371
+ };
372
+ signal.addEventListener('abort', onAbort, { once: true });
373
+
374
+ const pump = async () => {
375
+ try {
376
+ while (true) {
377
+ const { done, value } = await reader!.read();
378
+ if (done) break;
379
+ controller.enqueue(value);
380
+ }
381
+ controller.close();
382
+ } catch (err) {
383
+ try { controller.error(err); } catch { /* already closed */ }
384
+ } finally {
385
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
386
+ signal.removeEventListener('abort', onAbort);
387
+ }
388
+ };
389
+ pump();
390
+ },
391
+ cancel() {
392
+ if (heartbeat) { clearInterval(heartbeat); heartbeat = undefined; }
393
+ reader?.cancel();
394
+ },
395
+ });
396
+
397
+ return new Response(wrapped, {
398
+ status: original.status,
399
+ headers: original.headers,
400
+ });
401
+ }
299
402
  }
package/src/mcp/server.ts CHANGED
@@ -48,31 +48,22 @@ export class McpServer {
48
48
  }
49
49
 
50
50
  /**
51
- * Create an SSE response that keeps the connection open for streaming
52
- * This is the SSE transport endpoint
51
+ * Create an SSE response that keeps the connection open for streaming.
52
+ *
53
+ * Heartbeat / keep-alive pings are handled automatically by the framework's
54
+ * SSE post-processor (`sseKeepAlive`), so this method no longer injects its
55
+ * own `setInterval`.
53
56
  */
54
57
  public createSseResponse(): Response {
55
- const registry = this.registry;
56
- const serverInfo = this.serverInfo;
57
58
  const encoder = new TextEncoder();
58
59
 
59
60
  const stream = new ReadableStream({
60
61
  start(controller) {
61
- // Send initial server info event
62
62
  const initEvent = `event: endpoint\ndata: ${JSON.stringify({
63
63
  type: 'endpoint',
64
64
  method: 'POST',
65
65
  })}\n\n`;
66
66
  controller.enqueue(encoder.encode(initEvent));
67
-
68
- // Keep connection alive with periodic pings
69
- const pingInterval = setInterval(() => {
70
- try {
71
- controller.enqueue(encoder.encode(': ping\n\n'));
72
- } catch (_error) {
73
- clearInterval(pingInterval);
74
- }
75
- }, 15000);
76
67
  },
77
68
  });
78
69
 
@@ -81,7 +72,7 @@ export class McpServer {
81
72
  'Content-Type': 'text/event-stream',
82
73
  'Cache-Control': 'no-cache',
83
74
  'Connection': 'keep-alive',
84
- 'X-MCP-Server': `${serverInfo.name}/${serverInfo.version}`,
75
+ 'X-MCP-Server': `${this.serverInfo.name}/${this.serverInfo.version}`,
85
76
  },
86
77
  });
87
78
  }