acrylblends 1.0.10 → 1.2.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.
Files changed (252) hide show
  1. package/README.md +9 -0
  2. package/dist/index.d.ts +60 -2
  3. package/dist/index.js +1992 -33
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/agents/index.md +1 -1
  6. package/docs/framework/api/summary.md +1 -1
  7. package/docs/framework/getting-started/desktop.md +23 -4
  8. package/docs/framework/guides/create-acrylblends.md +1 -1
  9. package/docs/framework/guides/production.md +20 -0
  10. package/docs/framework/workshop/README.md +174 -285
  11. package/docs/framework/workshop/facilitator.md +81 -90
  12. package/legacy/README.md +9 -0
  13. package/legacy/agent/157ce3db1112/src/app.ts +98 -0
  14. package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
  15. package/legacy/agent/24c681a00fca/src/app.ts +160 -0
  16. package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
  17. package/legacy/agent/2e790da977b2/src/app.ts +98 -0
  18. package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
  19. package/legacy/agent/6307045545dc/src/app.ts +168 -0
  20. package/legacy/agent/6307045545dc/src/routes.ts +83 -0
  21. package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
  22. package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
  23. package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
  24. package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
  25. package/legacy/agent/74ece148269c/src/app.ts +168 -0
  26. package/legacy/agent/74ece148269c/src/routes.ts +93 -0
  27. package/legacy/agent/855359304707/src/app.ts +168 -0
  28. package/legacy/agent/855359304707/src/routes.ts +126 -0
  29. package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
  30. package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
  31. package/legacy/agent/c2f30958043b/src/app.ts +98 -0
  32. package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
  33. package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
  34. package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
  35. package/legacy/blank/aea589283445/src/routes.ts +16 -0
  36. package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
  37. package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
  38. package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
  39. package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
  40. package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
  41. package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
  42. package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
  43. package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
  44. package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
  45. package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
  46. package/legacy/layouts.json +524 -0
  47. package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
  48. package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
  49. package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
  50. package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
  51. package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
  52. package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
  53. package/legacy/notes/4edc36949c15/src/app.ts +107 -0
  54. package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
  55. package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
  56. package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
  57. package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
  58. package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
  59. package/legacy/todo/02e613b3625c/src/app.ts +71 -0
  60. package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
  61. package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
  62. package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
  63. package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
  64. package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
  65. package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
  66. package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
  67. package/legacy/todo/8e64211312f3/src/app.ts +71 -0
  68. package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
  69. package/legacy/todo/b79078a87565/src/app.ts +71 -0
  70. package/legacy/todo/b79078a87565/src/todo.ts +125 -0
  71. package/lib/{audit-Dw-siYTp.d.ts → audit-Bok_UNCn.d.ts} +41 -6
  72. package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
  73. package/lib/cordis/door.d.ts +44 -7
  74. package/lib/cordis/door.js +214 -60
  75. package/lib/cordis/inspect.js +1 -1
  76. package/lib/cordis/node-server.d.ts +101 -3
  77. package/lib/cordis/node-server.js +3 -3
  78. package/lib/cordis/router-http.d.ts +7 -1
  79. package/lib/cordis/router-http.js +44 -3
  80. package/lib/cordis/router.d.ts +1 -1
  81. package/lib/cordis/router.js +1 -1
  82. package/lib/cordis/state.d.ts +3 -2
  83. package/lib/cordis/state.js +371 -7
  84. package/lib/door/host.d.ts +2 -2
  85. package/lib/door/host.js +1 -1
  86. package/lib/door/page.d.ts +20 -1
  87. package/lib/door/page.js +75 -14
  88. package/lib/door/protocol.d.ts +1 -1
  89. package/lib/drivers.d.ts +10 -1
  90. package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
  91. package/lib/node-server.d.ts +7 -1
  92. package/lib/node-server.js +2 -2
  93. package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
  94. package/lib/{snapshot-text-BfEbEOjA.js → snapshot-text-CXTnjxGx.js} +121 -9
  95. package/lib/src-CtkIX_Gg.js +427 -0
  96. package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
  97. package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
  98. package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
  99. package/package.json +2 -1
  100. package/plugins/acryl-door/src/host/channel.ts +124 -9
  101. package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
  102. package/plugins/acryl-door/src/host/door-file.ts +7 -2
  103. package/plugins/acryl-door/src/host/door.ts +58 -23
  104. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  105. package/plugins/acryl-door/src/host/page-socket.ts +18 -8
  106. package/plugins/acryl-door/src/host/verbs.ts +3 -0
  107. package/plugins/acryl-door/src/index.ts +166 -29
  108. package/plugins/acryl-door/src/page/base.ts +32 -0
  109. package/plugins/acryl-door/src/page/channel.ts +11 -1
  110. package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
  111. package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
  112. package/plugins/acryl-door/src/page/driver/driver.ts +49 -7
  113. package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
  114. package/plugins/acryl-door/src/page/index.ts +46 -2
  115. package/plugins/acryl-door/src/page/indicator.ts +14 -6
  116. package/plugins/acryl-door/src/page/outline.ts +56 -0
  117. package/plugins/acryl-door/src/page/protocol.ts +4 -0
  118. package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
  119. package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
  120. package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
  121. package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
  122. package/plugins/acryl-node-server/src/client-modules.ts +7 -1
  123. package/plugins/acryl-node-server/src/index.ts +304 -16
  124. package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
  125. package/plugins/acryl-page/plugin.json +12 -0
  126. package/plugins/acryl-page/src/index.ts +163 -0
  127. package/plugins/acryl-router/src/service.ts +77 -14
  128. package/plugins/acryl-router-http/src/index.ts +15 -1
  129. package/plugins/acryl-router-http/src/serve.ts +31 -0
  130. package/plugins/acryl-state/plugin.json +4 -3
  131. package/plugins/acryl-state/src/client-source.ts +244 -0
  132. package/plugins/acryl-state/src/service.ts +33 -6
  133. package/plugins/acryl-state/src/socket.ts +96 -0
  134. package/plugins/acryl-web-server/plugin.json +14 -0
  135. package/plugins/acryl-web-server/src/index.ts +164 -0
  136. package/plugins/agent/agent.ts +38 -7
  137. package/plugins/agent/index.ts +1 -1
  138. package/plugins/agent/model.ts +10 -2
  139. package/plugins/agent/tools.ts +9 -3
  140. package/plugins/agent-panel/client/describe.ts +57 -0
  141. package/plugins/agent-panel/client/driver-pick.ts +22 -0
  142. package/plugins/agent-panel/client/frame.ts +51 -0
  143. package/plugins/agent-panel/client/main.ts +343 -81
  144. package/plugins/agent-panel/drivers.ts +7 -2
  145. package/plugins/agent-panel/headless.ts +5 -2
  146. package/plugins/agent-panel/index.ts +187 -61
  147. package/plugins/agent-panel/mount.ts +27 -0
  148. package/plugins/agent-panel/visitors.ts +231 -0
  149. package/plugins/desktop/frame.ts +231 -0
  150. package/plugins/desktop/launch.ts +44 -9
  151. package/plugins/desktop/main.ts +100 -32
  152. package/plugins/desktop/window.ts +21 -4
  153. package/plugins/driver-pty/client/terminal.ts +4 -1
  154. package/plugins/driver-pty/index.ts +38 -11
  155. package/plugins/driver-pty/spawn.ts +38 -0
  156. package/plugins/driver-pty/terminal.ts +2 -0
  157. package/plugins/extensions/index.ts +26 -1
  158. package/plugins/extensions/resolve.ts +13 -0
  159. package/plugins/feedback/client.d.ts +22 -0
  160. package/plugins/feedback/client.js +471 -0
  161. package/plugins/feedback/index.ts +143 -0
  162. package/plugins/feedback/plugin.json +11 -0
  163. package/plugins/feedback/store.ts +307 -0
  164. package/plugins/home/index.ts +122 -0
  165. package/plugins/home/plugin.json +11 -0
  166. package/plugins/instance/index.ts +12 -3
  167. package/plugins/manifest/app-plugins.ts +86 -0
  168. package/plugins/manifest/index.ts +12 -2
  169. package/plugins/manifest/snapshot.ts +275 -0
  170. package/plugins/openai/index.ts +64 -5
  171. package/plugins/openai/prompt.ts +23 -0
  172. package/shared/AGENTS.core.md +36 -16
  173. package/shared/skills/acryl-add-ui/SKILL.md +19 -0
  174. package/shared/skills/acryl-app/SKILL.md +13 -0
  175. package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
  176. package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
  177. package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
  178. package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
  179. package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
  180. package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
  181. package/shared/skills/cordis-plugin/SKILL.md +26 -0
  182. package/templates/agent/AGENTS.md +4 -2
  183. package/templates/agent/blend.yaml +4 -0
  184. package/templates/agent/gitignore +1 -0
  185. package/templates/agent/package.json +1 -0
  186. package/templates/agent/src/app.ts +150 -33
  187. package/templates/agent/src/frame.ts +17 -0
  188. package/templates/agent/src/main.ts +5 -3
  189. package/templates/agent/src/routes.ts +73 -21
  190. package/templates/agent/template.json +2 -0
  191. package/templates/agent/test/app.test.ts +83 -5
  192. package/templates/blank/AGENTS.md +3 -2
  193. package/templates/blank/blend.yaml +11 -0
  194. package/templates/blank/gitignore +1 -0
  195. package/templates/blank/package.json +1 -0
  196. package/templates/blank/src/app.ts +100 -29
  197. package/templates/blank/src/door.ts +7 -0
  198. package/templates/blank/src/first-page.ts +5 -0
  199. package/templates/blank/src/frame.ts +17 -0
  200. package/templates/blank/src/routes.ts +65 -8
  201. package/templates/blank/template.json +14 -1
  202. package/templates/blank/test/app.test.ts +103 -16
  203. package/templates/chat/blend.yaml +2 -1
  204. package/templates/chat/gitignore +1 -0
  205. package/templates/chat/package.json +1 -0
  206. package/templates/chat/src/app.ts +72 -22
  207. package/templates/chat/src/chat-app.ts +11 -2
  208. package/templates/chat/src/client/chat.tsx +4 -2
  209. package/templates/chat/src/frame.ts +17 -0
  210. package/templates/chat/src/main.ts +1 -6
  211. package/templates/chat/src/page.tsx +6 -4
  212. package/templates/chat/src/routes.ts +14 -4
  213. package/templates/chat/src/runtime.ts +9 -3
  214. package/templates/chat/template.json +3 -1
  215. package/templates/chat/test/chat.test.ts +46 -1
  216. package/templates/chat/test/server.test.ts +17 -3
  217. package/templates/nodejs/blend.yaml +2 -1
  218. package/templates/nodejs/gitignore +1 -0
  219. package/templates/nodejs/package.json +1 -0
  220. package/templates/nodejs/src/app.ts +76 -21
  221. package/templates/nodejs/src/frame.ts +17 -0
  222. package/templates/nodejs/src/main.ts +1 -6
  223. package/templates/nodejs/src/page.tsx +3 -3
  224. package/templates/nodejs/src/routes.ts +32 -13
  225. package/templates/nodejs/template.json +3 -1
  226. package/templates/nodejs/test/app.test.ts +38 -3
  227. package/templates/nodejs/test/server.test.ts +17 -3
  228. package/templates/notes/blend.yaml +2 -1
  229. package/templates/notes/gitignore +1 -0
  230. package/templates/notes/package.json +1 -0
  231. package/templates/notes/src/app.ts +72 -22
  232. package/templates/notes/src/frame.ts +17 -0
  233. package/templates/notes/src/main.ts +1 -6
  234. package/templates/notes/src/notes-app.ts +24 -17
  235. package/templates/notes/src/routes.ts +11 -4
  236. package/templates/notes/src/runtime.ts +9 -3
  237. package/templates/notes/src/views.tsx +26 -17
  238. package/templates/notes/template.json +3 -1
  239. package/templates/notes/test/notes.test.ts +39 -1
  240. package/templates/notes/test/server.test.ts +17 -3
  241. package/templates/todo/blend.yaml +3 -0
  242. package/templates/todo/gitignore +1 -0
  243. package/templates/todo/package.json +1 -0
  244. package/templates/todo/src/app.ts +81 -27
  245. package/templates/todo/src/door.ts +7 -0
  246. package/templates/todo/src/frame.ts +17 -0
  247. package/templates/todo/src/page.ts +13 -6
  248. package/templates/todo/src/todo.ts +18 -6
  249. package/templates/todo/template.json +11 -1
  250. package/templates/todo/test/todo.test.ts +67 -9
  251. package/lib/src-LX0E4ec4.js +0 -117
  252. package/shared/SKILL.md +0 -14
@@ -0,0 +1,427 @@
1
+ import { c as RAW_RESPONSE_HANDLED, i as createAdaptorServer, l as RAW_UPGRADE_HANDLED, r as UpgradeHub, u as Request } from "./test-websocket-CQOSvBth.js";
2
+ import "./serve-static-C1Nkj_xB.js";
3
+ import { Service } from "@deepseek-ai/cordis";
4
+ import z from "@deepseek-ai/schemastery";
5
+ //#region ../../plugins/acryl-node-server/src/index.ts
6
+ /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
7
+ const name = "acryl-node-server";
8
+ const inject = ["router"];
9
+ /** The pathname of a request target, for raw matching: a malformed one matches nothing (`'/'` matches only itself). */
10
+ function pathnameOf(target) {
11
+ if (target === void 0) return "/";
12
+ try {
13
+ return new URL(target, "http://node-server.invalid").pathname;
14
+ } catch {
15
+ return "/";
16
+ }
17
+ }
18
+ /** Every field has a default, so a caller passes only what it changes: `ctx.plugin(plugin, { ... })` takes the partial shape. */
19
+ const Config = z.object({
20
+ hostname: z.string().default("127.0.0.1").description("Address to bind. Loopback by default; binding elsewhere exposes the app to the network."),
21
+ port: z.number().min(0).max(65535).default(0).description("Port to listen on; 0 lets the system choose. A taken port, or one a browser refuses, is followed by the next usable one."),
22
+ scan: z.number().min(0).default(20).description("How many further ports to try when the configured one is taken or a browser refuses it."),
23
+ websocket: z.object({
24
+ maxPayload: z.number().min(1).default(1024 * 1024).description("The largest WebSocket frame accepted, in bytes; a bigger one closes the socket with 1009."),
25
+ heartbeat: z.number().min(0).default(3e4).description("Milliseconds between pings; a peer that did not answer the last one is terminated. 0 turns it off."),
26
+ grace: z.number().min(0).default(2e3).description("Milliseconds a closing socket gets before it is terminated when its plugin unloads.")
27
+ }).default({})
28
+ });
29
+ /**
30
+ * Milliseconds a request already in flight, or an upgraded socket's own owner, may finish once the server stops taking new work. A save during a
31
+ * restart must not be cut; a stream that never ends must not hold the stop open.
32
+ */
33
+ const STOP_GRACE_MS = 2e3;
34
+ var NodeServer = class extends Service {
35
+ hostname;
36
+ config;
37
+ server;
38
+ router;
39
+ hub;
40
+ bound = 0;
41
+ /** The one stop this server runs: a second call (the disposer after an explicit stop) waits on the same promise. */
42
+ stopping;
43
+ /** Requests whose response has not finished yet: the stop waits for them before their sockets are cut. */
44
+ active = 0;
45
+ /** Upgraded sockets this server handed out (WebSockets): neither close() nor closeAllConnections() reaches them, so the stop tracks them itself. */
46
+ upgraded = /* @__PURE__ */ new Set();
47
+ /** Raw routes registered by the host's compatibility layer (see {@link registerRaw}). */
48
+ rawExact = /* @__PURE__ */ new Map();
49
+ rawPrefix = /* @__PURE__ */ new Map();
50
+ rawUpgrades = /* @__PURE__ */ new Map();
51
+ /** Gates registered by a host that maps an app's URLs (see {@link NodeServer.gate}); they run in the order they were added. */
52
+ gates = [];
53
+ /** One raw dispatch per upgrade request: a second listener (or a re-added one) must never emit the same request again. */
54
+ rawUpgradeSeen = /* @__PURE__ */ new WeakSet();
55
+ constructor(ctx, config) {
56
+ super(ctx, "nodeServer");
57
+ this.config = config;
58
+ this.hostname = config.hostname;
59
+ const router = ctx.router;
60
+ this.router = router;
61
+ this.server = createAdaptorServer({
62
+ fetch: (request, bindings) => {
63
+ const { incoming, outgoing } = bindings;
64
+ this.active += 1;
65
+ let finished = false;
66
+ const settle = () => {
67
+ if (finished) return;
68
+ finished = true;
69
+ this.active -= 1;
70
+ };
71
+ outgoing.once("finish", settle);
72
+ outgoing.once("close", settle);
73
+ if (incoming.headers.upgrade !== void 0) {
74
+ const websocket = incoming.headers.upgrade.toLowerCase() === "websocket";
75
+ return new Response("", {
76
+ status: websocket ? 503 : 400,
77
+ headers: { connection: "close" }
78
+ });
79
+ }
80
+ if (this.gates.length > 0) {
81
+ const requestLike = request;
82
+ const connection = {
83
+ remoteAddress: incoming.socket.remoteAddress,
84
+ localPort: incoming.socket.localPort
85
+ };
86
+ for (const gate of this.gates) {
87
+ const answer = gate(requestLike, connection);
88
+ if (answer !== void 0) return answer;
89
+ }
90
+ }
91
+ const raw = this.rawFor(incoming);
92
+ if (raw !== void 0) {
93
+ (async () => {
94
+ try {
95
+ await raw(incoming, outgoing);
96
+ } catch {
97
+ if (!outgoing.headersSent) {
98
+ outgoing.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
99
+ outgoing.end("internal error");
100
+ } else outgoing.destroy();
101
+ }
102
+ })();
103
+ return RAW_RESPONSE_HANDLED;
104
+ }
105
+ return router.fetch(new Request(request), {
106
+ remoteAddress: incoming.socket.remoteAddress,
107
+ localPort: incoming.socket.localPort
108
+ });
109
+ },
110
+ hostname: config.hostname,
111
+ overrideGlobalObjects: false
112
+ });
113
+ this.server.on("upgrade", (request, socket, head) => {
114
+ if (this.rawUpgradeSeen.has(request)) return;
115
+ this.upgraded.add(socket);
116
+ socket.once("close", () => {
117
+ this.upgraded.delete(socket);
118
+ });
119
+ socket.on("error", () => {
120
+ socket.destroy();
121
+ });
122
+ const websocket = request.headers.upgrade?.toLowerCase() === "websocket";
123
+ const handler = this.rawUpgrades.get(pathnameOf(request.url));
124
+ if (handler === void 0) {
125
+ if (!websocket || this.hub !== void 0) return;
126
+ if (socket.destroyed || socket.writableEnded) return;
127
+ request[RAW_UPGRADE_HANDLED] = true;
128
+ socket.end("HTTP/1.1 503 Service Unavailable\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
129
+ return;
130
+ }
131
+ if (!websocket) {
132
+ if (socket.destroyed || socket.writableEnded) return;
133
+ request[RAW_UPGRADE_HANDLED] = true;
134
+ socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
135
+ return;
136
+ }
137
+ this.rawUpgradeSeen.add(request);
138
+ request[RAW_UPGRADE_HANDLED] = true;
139
+ Promise.resolve(handler(request, socket, head)).catch(() => socket.destroy());
140
+ });
141
+ ctx.effect(() => () => this.stop(), "acryl-node-server: close listener and open connections");
142
+ }
143
+ /**
144
+ * Stop serving, gracefully, and resolve once the port is free: no new connections; requests already in flight may finish (a stream that never ends is
145
+ * cut) within {@link STOP_GRACE_MS}; each upgraded socket's owner closes it with a close frame first, and what is left is destroyed. Idempotent - the
146
+ * disposer calls it.
147
+ *
148
+ * Calling this BEFORE the rest of the app is disposed is what makes the grace count: the router has its own disposal, and it aborts in-flight requests
149
+ * and ends their bodies by design, concurrently with every other disposer (Cordis runs them together). A launcher that disposes the context directly
150
+ * therefore cuts the request this drains; a launcher that stops the server first lets it answer, then disposes.
151
+ */
152
+ stop() {
153
+ this.stopping ??= this.closeGracefully();
154
+ return this.stopping;
155
+ }
156
+ async closeGracefully() {
157
+ const upgrades = this.hub?.close();
158
+ if (!this.server.listening) {
159
+ await upgrades;
160
+ return;
161
+ }
162
+ const stopped = new Promise((resolve) => {
163
+ this.server.close(() => {
164
+ resolve();
165
+ });
166
+ });
167
+ this.server.closeIdleConnections();
168
+ const settled = await this.settleStop(STOP_GRACE_MS);
169
+ this.server.closeIdleConnections();
170
+ if (!(settled ? await Promise.race([stopped.then(() => true), new Promise((resolve) => {
171
+ setTimeout(() => {
172
+ resolve(false);
173
+ }, 300);
174
+ })]) : false)) {
175
+ this.server.closeAllConnections();
176
+ for (const socket of this.upgraded) socket.destroy();
177
+ this.upgraded.clear();
178
+ }
179
+ await Promise.all([upgrades, stopped]);
180
+ }
181
+ /**
182
+ * Wait, bounded, until no request is in flight and no upgraded socket is left; true when both reached zero inside the bound. Every pass sweeps idle
183
+ * keep-alive sockets again: the single sweep `close()` does misses one that becomes idle after its request finished, and such a socket would otherwise
184
+ * hold the stop to its deadline.
185
+ */
186
+ async settleStop(ms) {
187
+ const deadline = Date.now() + ms;
188
+ while (Date.now() < deadline) {
189
+ if (this.active === 0 && this.upgraded.size === 0) return true;
190
+ this.server.closeIdleConnections();
191
+ await new Promise((resolve) => setTimeout(resolve, 25));
192
+ }
193
+ return this.active === 0 && this.upgraded.size === 0;
194
+ }
195
+ /**
196
+ * Serve a raw node handler at `path`, beside the router: `exact` matches the pathname only, `prefix` the path and everything below it. The
197
+ * handler gets the platform's `IncomingMessage`/`ServerResponse` and owns the response; the router is not run for it. A path already served
198
+ * throws. The returned remover restores the path (the server itself goes on serving the rest).
199
+ */
200
+ registerRaw(route) {
201
+ const table = route.kind === "exact" ? this.rawExact : this.rawPrefix;
202
+ if (table.has(route.path)) throw new Error(`acryl-node-server: ${route.path} is already served`);
203
+ table.set(route.path, route.handler);
204
+ return () => {
205
+ if (table.get(route.path) === route.handler) table.delete(route.path);
206
+ };
207
+ }
208
+ /**
209
+ * Answer matching requests before raw routes and the router, so a host can keep an app's URLs in scope under a prefix (the frame's canvas). A gate runs
210
+ * synchronously and can answer with a promise (see {@link RequestGate}); gates run in the order they were added, and the returned remover takes this one back.
211
+ */
212
+ gate(handler) {
213
+ this.gates.push(handler);
214
+ return () => {
215
+ const at = this.gates.indexOf(handler);
216
+ if (at !== -1) this.gates.splice(at, 1);
217
+ };
218
+ }
219
+ /**
220
+ * A gate's asynchronous answer: run the request it was given through the router, in the adapter's request form (a gate never makes one itself), with the
221
+ * socket facts. The raw routes do not run for that request.
222
+ */
223
+ throughRouter(request, connection = {}) {
224
+ return this.router.fetch(new Request(request), connection);
225
+ }
226
+ /**
227
+ * Claim an upgrade path for a raw node handler: the handler receives `(req, socket, head)` and from then on owns the socket. The router's
228
+ * WebSocket handling never sees a claimed upgrade. A path already claimed throws; the remover releases the claim. The transport's one raw
229
+ * listener exists for the server's whole lifetime and consults this registry, so claims coming and going never add or remove a listener.
230
+ */
231
+ registerRawUpgrade(route) {
232
+ if (this.rawUpgrades.has(route.path)) throw new Error(`acryl-node-server: the upgrade path ${route.path} is already claimed`);
233
+ this.rawUpgrades.set(route.path, route.handler);
234
+ return () => {
235
+ if (this.rawUpgrades.get(route.path) !== route.handler) return;
236
+ this.rawUpgrades.delete(route.path);
237
+ };
238
+ }
239
+ /** The raw handler for a request: an exact path first, then the longest matching prefix. */
240
+ rawFor(incoming) {
241
+ const path = pathnameOf(incoming.url);
242
+ const exact = this.rawExact.get(path);
243
+ if (exact !== void 0) return exact;
244
+ let best;
245
+ for (const [prefix, handler] of this.rawPrefix) if ((path === prefix || path.startsWith(`${prefix}/`)) && (best === void 0 || prefix.length > best.length)) best = {
246
+ length: prefix.length,
247
+ handler
248
+ };
249
+ return best?.handler;
250
+ }
251
+ /**
252
+ * `upgradeWebSocket` for the plugin that asks (call it once, in `apply`): `app.get('/ws', upgrade((c) => ({ onMessage(event, ws) { ... } })))`. Checks go in ordinary middleware on the
253
+ * route, and a handler that returns a `Response` refuses the upgrade with that status before any handshake. Every socket it accepts belongs to the calling plugin: when the plugin
254
+ * unloads they are closed with 1001 within `websocket.grace` and the handler sees `close` once. Pass `{ textOnly: true }` to drop binary frames. Needs the `ws` package.
255
+ */
256
+ upgrades() {
257
+ if (!this.hub) {
258
+ this.hub = new UpgradeHub(this.config.websocket);
259
+ this.hub.attach(this.server, this.ctx.router, { skipWhenRawUpgrade: true });
260
+ }
261
+ return this.hub.upgradeWebSocketFor(this.ctx);
262
+ }
263
+ /** How many `upgrade` listeners the transport holds (raw plus router setups); a test asserts it does not grow across claim changes. */
264
+ get upgradeListenerCount() {
265
+ return this.server.listenerCount("upgrade");
266
+ }
267
+ /** How many WebSocket connections are open now. */
268
+ get sockets() {
269
+ return this.hub?.size ?? 0;
270
+ }
271
+ /** The port in use, 0 before the server listens. */
272
+ get port() {
273
+ return this.bound;
274
+ }
275
+ /** The address clients use. */
276
+ get url() {
277
+ return `http://${this.hostname.includes(":") ? `[${this.hostname}]` : this.hostname}:${String(this.bound)}`;
278
+ }
279
+ /** Bind, scanning past taken ports and past the ports a browser refuses. */
280
+ async listen() {
281
+ const { port, scan, hostname } = this.config;
282
+ const last = port === 0 ? 0 : Math.min(65535, port + scan);
283
+ for (const candidate of portCandidates(port, scan)) try {
284
+ await new Promise((resolve, reject) => {
285
+ const onError = (error) => {
286
+ reject(error);
287
+ };
288
+ this.server.once("error", onError);
289
+ this.server.listen(candidate, hostname, () => {
290
+ this.server.off("error", onError);
291
+ resolve();
292
+ });
293
+ });
294
+ this.bound = this.server.address().port;
295
+ return;
296
+ } catch (error) {
297
+ if (!isPortUnavailable(error)) throw error;
298
+ }
299
+ throw new Error(`acryl-node-server: no usable port from ${String(port)} to ${String(last)} on ${hostname}: each is taken, or one a browser refuses`);
300
+ }
301
+ };
302
+ /**
303
+ * The ports every Fetch client refuses to connect to: the Fetch standard's bad ports (https://fetch.spec.whatwg.org/#port-blocking). Chromium
304
+ * refuses a page on one (ERR_UNSAFE_PORT) and Node's fetch answers "bad port"; the list below is the one Node 24 reports, probed port by port
305
+ * on 2026-11-03 (82 ports, 1 to 10080). An app's stable port is hashed into 3100..3999, and 3659 is on the list: without this rule one app in
306
+ * 900 would come up on an address no browser may open, which the frame gate caught that day (verify-frame-token was refused at 3659).
307
+ */
308
+ const RESTRICTED_PORTS = /* @__PURE__ */ new Set([
309
+ 1,
310
+ 7,
311
+ 9,
312
+ 11,
313
+ 13,
314
+ 15,
315
+ 17,
316
+ 19,
317
+ 20,
318
+ 21,
319
+ 22,
320
+ 23,
321
+ 25,
322
+ 37,
323
+ 42,
324
+ 43,
325
+ 53,
326
+ 69,
327
+ 77,
328
+ 79,
329
+ 87,
330
+ 95,
331
+ 101,
332
+ 102,
333
+ 103,
334
+ 104,
335
+ 109,
336
+ 110,
337
+ 111,
338
+ 113,
339
+ 115,
340
+ 117,
341
+ 119,
342
+ 123,
343
+ 135,
344
+ 137,
345
+ 139,
346
+ 143,
347
+ 161,
348
+ 179,
349
+ 389,
350
+ 427,
351
+ 465,
352
+ 512,
353
+ 513,
354
+ 514,
355
+ 515,
356
+ 526,
357
+ 530,
358
+ 531,
359
+ 532,
360
+ 540,
361
+ 548,
362
+ 554,
363
+ 556,
364
+ 563,
365
+ 587,
366
+ 601,
367
+ 636,
368
+ 989,
369
+ 990,
370
+ 993,
371
+ 995,
372
+ 1719,
373
+ 1720,
374
+ 1723,
375
+ 2049,
376
+ 3659,
377
+ 4045,
378
+ 4190,
379
+ 5060,
380
+ 5061,
381
+ 6e3,
382
+ 6566,
383
+ 6665,
384
+ 6666,
385
+ 6667,
386
+ 6668,
387
+ 6669,
388
+ 6679,
389
+ 6697,
390
+ 10080
391
+ ]);
392
+ /** Whether a browser refuses to open this port. */
393
+ function isRestrictedPort(port) {
394
+ return RESTRICTED_PORTS.has(port);
395
+ }
396
+ /**
397
+ * The ports `listen` may try, in order: `port` with at most `scan` further ones, without the ports a browser refuses. Port 0 stays alone: the
398
+ * system chooses it, and the ports it hands out are above the restricted range.
399
+ */
400
+ function portCandidates(port, scan) {
401
+ const last = port === 0 ? 0 : Math.min(65535, port + scan);
402
+ const candidates = [];
403
+ for (let candidate = port; candidate <= last; candidate += 1) if (!isRestrictedPort(candidate)) candidates.push(candidate);
404
+ return candidates;
405
+ }
406
+ /**
407
+ * Whether a failed bind means "this port, try the next one". A port another socket holds answers `EADDRINUSE` everywhere. On Windows a port
408
+ * inside a range the system reserves (Hyper-V, WSL and Docker exclude ranges, `netsh interface ipv4 show excludedportrange`) answers
409
+ * `EACCES`, which is a property of that port too; elsewhere `EACCES` means the process may not bind low ports at all, so moving on would
410
+ * only hide the reason.
411
+ */
412
+ function isPortUnavailable(error, platform = process.platform) {
413
+ const code = error?.code;
414
+ return code === "EADDRINUSE" || platform === "win32" && code === "EACCES";
415
+ }
416
+ /** Loads once the server listens, so a plugin that injects `nodeServer` can rely on `url`. A failure to bind fails the plugin. */
417
+ async function apply(ctx, config = {}) {
418
+ await new NodeServer(ctx, {
419
+ hostname: "127.0.0.1",
420
+ port: 0,
421
+ scan: 20,
422
+ websocket: {},
423
+ ...config
424
+ }).listen();
425
+ }
426
+ //#endregion
427
+ export { apply as a, isRestrictedPort as c, STOP_GRACE_MS as i, name as l, NodeServer as n, inject as o, RESTRICTED_PORTS as r, isPortUnavailable as s, Config as t, portCandidates as u };
@@ -5,6 +5,8 @@ import { t as TrieRouter } from "./trie-router-JydLKUa2.js";
5
5
  import { t as Acryl } from "./app-CQcGWjrk.js";
6
6
  import "./core-BOu1jYua.js";
7
7
  import "./connection-C7xgP1hP.js";
8
+ import { appendFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
8
10
  import { Service } from "@deepseek-ai/cordis";
9
11
  import z from "@deepseek-ai/schemastery";
10
12
  //#region ../../plugins/acryl-router/src/service.ts
@@ -17,6 +19,28 @@ import z from "@deepseek-ai/schemastery";
17
19
  * response body still streaming and waits for all of it to settle.
18
20
  */
19
21
  const DEFAULT_DISPOSE_GRACE_MS = 2e3;
22
+ /** How long disposal waits for the flights it had to abort after the drain, before it continues without them. */
23
+ const ABORT_SETTLE_MS = 300;
24
+ /** Set by ACRYL_ROUTER_TRACE=1: append request-signal lifecycle lines to ACRYL_ROUTER_TRACE_FILE (the CI artifact) - diagnostics for the abort-reaches-handler defect. */
25
+ const traceFile = () => process.env.ACRYL_ROUTER_TRACE_FILE ?? `${tmpdir()}/acryl-router-trace.log`;
26
+ const trace = (line) => {
27
+ if (process.env.ACRYL_ROUTER_TRACE !== "1") return;
28
+ try {
29
+ appendFileSync(traceFile(), `${String(Date.now())} [${String(process.pid)}] ${line}\n`);
30
+ } catch {}
31
+ };
32
+ let flightSeq = 0;
33
+ const SIGNAL_TAG = Symbol.for("acryl.router.signalTag");
34
+ const tagOf = (signal) => {
35
+ if (signal === void 0) return "none";
36
+ const tag = signal[SIGNAL_TAG];
37
+ return typeof tag === "string" ? tag : "untagged";
38
+ };
39
+ const tagSignal = (signal, tag) => {
40
+ try {
41
+ signal[SIGNAL_TAG] = tag;
42
+ } catch {}
43
+ };
20
44
  /** Every field has a default, so a caller passes only what it changes: `ctx.plugin(plugin, { ... })` takes the partial shape. */
21
45
  const Config = z.object({
22
46
  trailingSlash: z.union(["strict", "ignore"]).default("strict").description("Whether `/a` and `/a/` are different paths."),
@@ -64,8 +88,8 @@ var Router = class extends Service {
64
88
  };
65
89
  ctx.effect(() => async () => {
66
90
  this.state.closing = true;
67
- await this.end([...this.flights], "disposal");
68
- }, "acryl-router: abort and settle in-flight requests");
91
+ await this.drain();
92
+ }, "acryl-router: drain in-flight requests, then abort what is left");
69
93
  }
70
94
  /** Registrations currently held (mounts, and the router-wide error and not-found handlers); a leak check for reload tests. */
71
95
  get size() {
@@ -83,7 +107,7 @@ var Router = class extends Service {
83
107
  const remove = () => {
84
108
  if (!this.state.mounts.delete(mount)) return Promise.resolve();
85
109
  this.state.composed = void 0;
86
- return this.end([...this.flights].filter((flight) => (flight.running.get(mount) ?? 0) > 0 || flight.producer === mount), `removing ${path}`);
110
+ return this.drainFlights([...this.flights].filter((flight) => (flight.running.get(mount) ?? 0) > 0 || flight.producer === mount), `removing ${path}`);
87
111
  };
88
112
  const served = new Set(app.routes.map((route) => `${route.method} ${mergePath(path, route.path)}`));
89
113
  const owner = this.ctx;
@@ -125,6 +149,7 @@ var Router = class extends Service {
125
149
  const handler = async (c, next) => {
126
150
  const flight = this.flightOf.get(c.req.raw);
127
151
  if (flight === void 0) return inner(c, next);
152
+ trace(`handler ${flight.tag} requestTag=${tagOf(c.req.raw.signal)} innerTag=${tagOf(flight.inner.signal)} sameSignal=${String(c.req.raw.signal === flight.inner.signal)} aborted=${String(c.req.raw.signal.aborted)}`);
128
153
  let calledNext = false;
129
154
  flight.running.set(mount, (flight.running.get(mount) ?? 0) + 1);
130
155
  try {
@@ -145,22 +170,52 @@ var Router = class extends Service {
145
170
  }
146
171
  return mount;
147
172
  }
148
- /** Abort `flights`, cut their bodies short and wait for them to settle, at most `disposeGraceMs`. */
149
- async end(flights, why) {
173
+ /**
174
+ * The router's own disposal: let the flights already in flight finish within `disposeGraceMs` - a save being written when the app stops must not be cut
175
+ * by the router beneath it - then abort what is left and settle it for a moment, so disposal does not wait a second full grace on code that ignores
176
+ * its abort signal.
177
+ */
178
+ async drain() {
179
+ await this.drainFlights([...this.flights], "disposal");
180
+ }
181
+ /** The one draining rule: let `flights` finish inside the grace, then abort and settle what is left. */
182
+ async drainFlights(flights, why) {
183
+ trace(`drain ${why} chosen=[${flights.map((flight) => flight.tag).join(", ")}]`);
150
184
  if (flights.length === 0) return;
151
- for (const flight of flights) {
152
- flight.controller.abort();
153
- flight.endBody();
185
+ if (await this.settledWithin(flights, this.graceMs)) {
186
+ trace(`drain ${why} settled inside the grace`);
187
+ return;
154
188
  }
189
+ const left = flights.filter((flight) => this.flights.has(flight));
190
+ trace(`drain ${why} grace over left=[${left.map((flight) => flight.tag).join(", ")}]`);
191
+ if (left.length > 0) await this.end(left, why, ABORT_SETTLE_MS);
192
+ }
193
+ /** Resolve true when every flight settled inside `ms`, false at the deadline. */
194
+ async settledWithin(flights, ms) {
155
195
  let timer;
156
196
  const gaveUp = new Promise((resolve) => {
157
197
  timer = setTimeout(() => {
158
- resolve("gave-up");
159
- }, this.graceMs);
198
+ resolve(false);
199
+ }, ms);
160
200
  });
161
- const outcome = await Promise.race([Promise.allSettled(flights.map((flight) => flight.settled)).then(() => "settled"), gaveUp]);
201
+ const outcome = await Promise.race([Promise.allSettled(flights.map((flight) => flight.settled)).then(() => true), gaveUp]);
162
202
  clearTimeout(timer);
163
- if (outcome === "gave-up") this.ctx.logger.warn(`acryl-router: ${String(flights.length)} request(s) did not finish ${String(this.graceMs)} ms after abort (${why}); continued without them`);
203
+ return outcome;
204
+ }
205
+ /** Abort `flights`, cut their bodies short and wait for them to settle, at most `settleMs`. */
206
+ async end(flights, why, settleMs = this.graceMs) {
207
+ if (flights.length === 0) return;
208
+ for (const flight of flights) {
209
+ trace(`abort ${flight.tag} before=${String(flight.controller.signal.aborted)}`);
210
+ flight.controller.abort();
211
+ trace(`abort ${flight.tag} after=${String(flight.controller.signal.aborted)} innerAborted=${String(flight.inner.signal.aborted)}`);
212
+ queueMicrotask(() => {
213
+ trace(`post-abort ${flight.tag} innerAborted=${String(flight.inner.signal.aborted)}`);
214
+ });
215
+ flight.endBody();
216
+ }
217
+ if (await this.settledWithin(flights, settleMs)) return;
218
+ this.ctx.logger.warn(`acryl-router: ${String(flights.length)} request(s) did not finish ${String(settleMs)} ms after abort (${why}); continued without them`);
164
219
  }
165
220
  /** Turn errors no mounted app handled into responses. One handler per router; the calling plugin owns it. */
166
221
  onError(handler) {
@@ -203,7 +258,10 @@ var Router = class extends Service {
203
258
  * `bindings` is an object an adapter hands to the handler as `c.env` (an upgrade request's socket hooks, for `upgradeWebSocket`); `connection` is added to it.
204
259
  */
205
260
  fetch(request, connection = {}, bindings) {
206
- if (this.state.closing) return Promise.resolve(new Response("Service Unavailable", { status: 503 }));
261
+ if (this.state.closing) return Promise.resolve(new Response("Service Unavailable", {
262
+ status: 503,
263
+ headers: { connection: "close" }
264
+ }));
207
265
  const controller = new AbortController();
208
266
  const forward = () => controller.abort();
209
267
  if (request.signal.aborted) controller.abort();
@@ -220,8 +278,13 @@ var Router = class extends Service {
220
278
  let bodyDone = Promise.resolve();
221
279
  let endBody = () => {};
222
280
  let resolveSettled;
281
+ const tag = `f${String(++flightSeq)}`;
282
+ const inner = new Request(request, { signal: controller.signal });
283
+ tagSignal(inner.signal, tag);
223
284
  const flight = {
224
285
  controller,
286
+ inner,
287
+ tag,
225
288
  running: /* @__PURE__ */ new Map(),
226
289
  producer: void 0,
227
290
  endBody: () => {
@@ -231,7 +294,7 @@ var Router = class extends Service {
231
294
  resolveSettled = resolve;
232
295
  })
233
296
  };
234
- const inner = new Request(request, { signal: controller.signal });
297
+ trace(`create ${tag} ${request.method} ${new URL(request.url).pathname}`);
235
298
  this.flightOf.set(inner, flight);
236
299
  const response = this.dispatch(inner, env, executionCtx).then((raw) => {
237
300
  const tracked = this.track(raw);
@@ -744,6 +744,12 @@ Object.defineProperty(Response$1, "json", {
744
744
  configurable: true
745
745
  });
746
746
  //#endregion
747
+ //#region ../../plugins/acryl-node-server/src/adapter/raw.ts
748
+ /** Returned by a raw-aware fetch callback: the node response is already owned, do not write. */
749
+ const RAW_RESPONSE_HANDLED = Symbol.for("acryl-node-server.raw-response");
750
+ /** Set on an upgrade request a raw handler took over; the adapter's WebSocket setup skips it. */
751
+ const RAW_UPGRADE_HANDLED = Symbol.for("acryl-node-server.raw-upgrade");
752
+ //#endregion
747
753
  //#region ../../plugins/acryl-node-server/src/adapter/utils.ts
748
754
  async function readWithoutBlocking(readPromise) {
749
755
  return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]);
@@ -1040,6 +1046,7 @@ const getRequestListener = (fetchCallback, options = {}) => {
1040
1046
  incoming,
1041
1047
  outgoing
1042
1048
  });
1049
+ if (res === RAW_RESPONSE_HANDLED) return;
1043
1050
  if (!isPromise(res) && isImmediateCacheableResponse(res)) {
1044
1051
  if (needsBodyCleanup && !incoming.readableEnded) outgoing.once("finish", () => {
1045
1052
  if (!incoming.readableEnded) drainIncoming(incoming);
@@ -1137,6 +1144,7 @@ const appendResponseHeaders = (headers, responseHeaders) => {
1137
1144
  });
1138
1145
  };
1139
1146
  const rejectUpgradeRequest = (socket, status, responseHeaders) => {
1147
+ if (socket.destroyed || socket.writableEnded) return;
1140
1148
  const responseLines = ["Connection: close", "Content-Length: 0"];
1141
1149
  appendResponseHeaders(responseLines, responseHeaders);
1142
1150
  socket.end(`HTTP/1.1 ${status.toString()} ${STATUS_CODES[status] ?? ""}\r\n${responseLines.join("\r\n")}\r\n\r
@@ -1154,7 +1162,7 @@ const createUpgradeRequest = (request) => {
1154
1162
  return new Request(url, { headers });
1155
1163
  };
1156
1164
  const setupWebSocket = (options) => {
1157
- const { server, fetchCallback, wss } = options;
1165
+ const { server, fetchCallback, wss, skipWhenRawUpgrade = false } = options;
1158
1166
  const waiterMap = /* @__PURE__ */ new Map();
1159
1167
  wss.on("connection", (ws, request) => {
1160
1168
  const waiter = waiterMap.get(request);
@@ -1180,8 +1188,9 @@ const setupWebSocket = (options) => {
1180
1188
  });
1181
1189
  };
1182
1190
  server.on("upgrade", async (request, socket, head) => {
1191
+ if (skipWhenRawUpgrade && request[RAW_UPGRADE_HANDLED] === true) return;
1183
1192
  if (request.headers.upgrade?.toLowerCase() !== "websocket") {
1184
- if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 400);
1193
+ rejectUpgradeRequest(socket, 400);
1185
1194
  return;
1186
1195
  }
1187
1196
  const env = {
@@ -1199,13 +1208,13 @@ const setupWebSocket = (options) => {
1199
1208
  responseHeaders = response.headers;
1200
1209
  }
1201
1210
  } catch {
1202
- if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, 500);
1211
+ rejectUpgradeRequest(socket, 500);
1203
1212
  return;
1204
1213
  }
1205
1214
  const waiter = waiterMap.get(request);
1206
1215
  if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) {
1207
1216
  rejectWaiter(request);
1208
- if (server.listenerCount("upgrade") === 1) rejectUpgradeRequest(socket, status, responseHeaders);
1217
+ rejectUpgradeRequest(socket, status, responseHeaders);
1209
1218
  return;
1210
1219
  }
1211
1220
  const addResponseHeaders = (headers) => {
@@ -1389,7 +1398,7 @@ var UpgradeHub = class {
1389
1398
  * Accept upgrades from `source` and answer them with `router`: the request runs through the router's middleware with the socket's peer address and local port as connection
1390
1399
  * facts (so the same loopback, origin and host checks work), and the upgrade bindings travel in `c.env` for `upgradeWebSocket`.
1391
1400
  */
1392
- attach(source, router) {
1401
+ attach(source, router, options = {}) {
1393
1402
  const fetchCallback = (request, env) => {
1394
1403
  const { incoming } = env;
1395
1404
  return router.fetch(request, {
@@ -1400,7 +1409,8 @@ var UpgradeHub = class {
1400
1409
  setupWebSocket({
1401
1410
  server: source,
1402
1411
  fetchCallback,
1403
- wss: this.server
1412
+ wss: this.server,
1413
+ ...options
1404
1414
  });
1405
1415
  }
1406
1416
  /** How many sockets are open. */
@@ -1541,4 +1551,4 @@ async function testWebSocket(fetch, path, options = {}) {
1541
1551
  };
1542
1552
  }
1543
1553
  //#endregion
1544
- export { serve as a, Request$1 as c, createAdaptorServer as i, RequestError as l, UpgradeEmitter as n, upgradeWebSocket as o, UpgradeHub as r, getRequestListener as s, testWebSocket as t };
1554
+ export { serve as a, RAW_RESPONSE_HANDLED as c, RequestError as d, createAdaptorServer as i, RAW_UPGRADE_HANDLED as l, UpgradeEmitter as n, upgradeWebSocket as o, UpgradeHub as r, getRequestListener as s, testWebSocket as t, Request$1 as u };
@@ -88,7 +88,9 @@ declare class UpgradeHub {
88
88
  * Accept upgrades from `source` and answer them with `router`: the request runs through the router's middleware with the socket's peer address and local port as connection
89
89
  * facts (so the same loopback, origin and host checks work), and the upgrade bindings travel in `c.env` for `upgradeWebSocket`.
90
90
  */
91
- attach(source: UpgradeSource, router: UpgradeRouter): void;
91
+ attach(source: UpgradeSource, router: UpgradeRouter, options?: {
92
+ skipWhenRawUpgrade?: boolean;
93
+ }): void;
92
94
  /** How many sockets are open. */
93
95
  get size(): number;
94
96
  /** `upgradeWebSocket` for the plugin `owner`: the sockets it accepts are closed with 1001 when that plugin unloads. */