@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.
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -1
- package/dist/ai/providers/google-provider.d.ts.map +1 -1
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -1
- package/dist/ai/providers/openai-provider.d.ts.map +1 -1
- package/dist/ai/service.d.ts.map +1 -1
- package/dist/ai/types.d.ts +5 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/core/application.d.ts +17 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/server.d.ts +17 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/index.js +131 -41
- package/dist/mcp/server.d.ts +5 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/docs/idle-timeout.md +99 -8
- package/docs/zh/idle-timeout.md +97 -6
- package/package.json +1 -1
- package/src/ai/providers/anthropic-provider.ts +5 -2
- package/src/ai/providers/google-provider.ts +3 -0
- package/src/ai/providers/ollama-provider.ts +3 -0
- package/src/ai/providers/openai-provider.ts +5 -2
- package/src/ai/service.ts +17 -5
- package/src/ai/types.ts +5 -0
- package/src/core/application.ts +19 -0
- package/src/core/context.ts +7 -0
- package/src/core/server.ts +121 -18
- package/src/mcp/server.ts +6 -15
|
@@ -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;
|
|
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;
|
|
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;
|
|
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;
|
|
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"}
|
package/dist/ai/service.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -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
|
package/dist/ai/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ai/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/ai/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,UAAU,EAAE,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,MAAM,GAAG,YAAY,GAAG,QAAQ,GAAG,OAAO,CAAC;CAC1D;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE;QACd,KAAK,EAAE,MAAM,CAAC;QACd,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAClD;;OAEG;IACH,MAAM,CAAC,OAAO,EAAE,SAAS,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;IACvD;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,SAAS,EAAE,GAAG,MAAM,CAAC;IAC3C,oBAAoB;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,CAAC,GAAG,OAAO;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,MAAM,EAAE,CAAC,KAAK,WAAW,CAAC;IACzC,MAAM,EAAE,CAAC,CAAC;IACV,mCAAmC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,KAAK,CAAC,EAAE;QACN,gDAAgD;QAChD,YAAY,CAAC,EAAE,OAAO,CAAC;QACvB,2CAA2C;QAC3C,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED,eAAO,MAAM,gBAAgB,eAA0C,CAAC;AACxE,eAAO,MAAM,uBAAuB,eAA0C,CAAC;AAC/E,eAAO,MAAM,sBAAsB,eAAgD,CAAC;AAEpF;;GAEG;AACH,eAAO,MAAM,oBAAoB,+BAA+B,CAAC"}
|
|
@@ -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;
|
|
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"}
|
package/dist/core/context.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/core/server.d.ts
CHANGED
|
@@ -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;
|
|
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
|
|
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 =
|
|
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.
|
|
4339
|
-
|
|
4340
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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;
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -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
|
|
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"}
|
package/docs/idle-timeout.md
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
|
-
# idleTimeout
|
|
1
|
+
# idleTimeout & SSE Keep-Alive
|
|
2
2
|
|
|
3
|
-
|
|
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, //
|
|
24
|
+
idleTimeout: 15000, // 15 s — applies to all non-SSE connections
|
|
14
25
|
});
|
|
15
26
|
```
|
|
16
27
|
|
|
17
|
-
## Per-route timeout (
|
|
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
|
-
|
|
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
|
+
```
|
package/docs/zh/idle-timeout.md
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
|
-
# idleTimeout
|
|
1
|
+
# idleTimeout 与 SSE 保活
|
|
2
2
|
|
|
3
|
-
|
|
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`
|
|
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, //
|
|
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
|
-
|
|
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
|
@@ -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
|
|
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
|
|
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
|
/**
|
package/src/core/application.ts
CHANGED
|
@@ -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,
|
package/src/core/context.ts
CHANGED
|
@@ -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
|
/**
|
package/src/core/server.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
}
|