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.
- package/README.md +9 -0
- package/dist/index.d.ts +60 -2
- package/dist/index.js +1992 -33
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/api/summary.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +1 -1
- package/docs/framework/guides/production.md +20 -0
- package/docs/framework/workshop/README.md +174 -285
- package/docs/framework/workshop/facilitator.md +81 -90
- package/legacy/README.md +9 -0
- package/legacy/agent/157ce3db1112/src/app.ts +98 -0
- package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
- package/legacy/agent/24c681a00fca/src/app.ts +160 -0
- package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
- package/legacy/agent/2e790da977b2/src/app.ts +98 -0
- package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
- package/legacy/agent/6307045545dc/src/app.ts +168 -0
- package/legacy/agent/6307045545dc/src/routes.ts +83 -0
- package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
- package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
- package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
- package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
- package/legacy/agent/74ece148269c/src/app.ts +168 -0
- package/legacy/agent/74ece148269c/src/routes.ts +93 -0
- package/legacy/agent/855359304707/src/app.ts +168 -0
- package/legacy/agent/855359304707/src/routes.ts +126 -0
- package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
- package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
- package/legacy/agent/c2f30958043b/src/app.ts +98 -0
- package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
- package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
- package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
- package/legacy/blank/aea589283445/src/routes.ts +16 -0
- package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
- package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
- package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
- package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
- package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
- package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
- package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
- package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
- package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
- package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
- package/legacy/layouts.json +524 -0
- package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
- package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
- package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
- package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
- package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
- package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
- package/legacy/notes/4edc36949c15/src/app.ts +107 -0
- package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
- package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
- package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
- package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
- package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
- package/legacy/todo/02e613b3625c/src/app.ts +71 -0
- package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
- package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
- package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
- package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
- package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
- package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
- package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
- package/legacy/todo/8e64211312f3/src/app.ts +71 -0
- package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
- package/legacy/todo/b79078a87565/src/app.ts +71 -0
- package/legacy/todo/b79078a87565/src/todo.ts +125 -0
- package/lib/{audit-Dw-siYTp.d.ts → audit-Bok_UNCn.d.ts} +41 -6
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +44 -7
- package/lib/cordis/door.js +214 -60
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +101 -3
- package/lib/cordis/node-server.js +3 -3
- package/lib/cordis/router-http.d.ts +7 -1
- package/lib/cordis/router-http.js +44 -3
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.d.ts +3 -2
- package/lib/cordis/state.js +371 -7
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +20 -1
- package/lib/door/page.js +75 -14
- package/lib/door/protocol.d.ts +1 -1
- package/lib/drivers.d.ts +10 -1
- package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
- package/lib/node-server.d.ts +7 -1
- package/lib/node-server.js +2 -2
- package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
- package/lib/{snapshot-text-BfEbEOjA.js → snapshot-text-CXTnjxGx.js} +121 -9
- package/lib/src-CtkIX_Gg.js +427 -0
- package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
- package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
- package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
- package/package.json +2 -1
- package/plugins/acryl-door/src/host/channel.ts +124 -9
- package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
- package/plugins/acryl-door/src/host/door-file.ts +7 -2
- package/plugins/acryl-door/src/host/door.ts +58 -23
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/page-socket.ts +18 -8
- package/plugins/acryl-door/src/host/verbs.ts +3 -0
- package/plugins/acryl-door/src/index.ts +166 -29
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +11 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +49 -7
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +46 -2
- package/plugins/acryl-door/src/page/indicator.ts +14 -6
- package/plugins/acryl-door/src/page/outline.ts +56 -0
- package/plugins/acryl-door/src/page/protocol.ts +4 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
- package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
- package/plugins/acryl-node-server/src/client-modules.ts +7 -1
- package/plugins/acryl-node-server/src/index.ts +304 -16
- package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
- package/plugins/acryl-page/plugin.json +12 -0
- package/plugins/acryl-page/src/index.ts +163 -0
- package/plugins/acryl-router/src/service.ts +77 -14
- package/plugins/acryl-router-http/src/index.ts +15 -1
- package/plugins/acryl-router-http/src/serve.ts +31 -0
- package/plugins/acryl-state/plugin.json +4 -3
- package/plugins/acryl-state/src/client-source.ts +244 -0
- package/plugins/acryl-state/src/service.ts +33 -6
- package/plugins/acryl-state/src/socket.ts +96 -0
- package/plugins/acryl-web-server/plugin.json +14 -0
- package/plugins/acryl-web-server/src/index.ts +164 -0
- package/plugins/agent/agent.ts +38 -7
- package/plugins/agent/index.ts +1 -1
- package/plugins/agent/model.ts +10 -2
- package/plugins/agent/tools.ts +9 -3
- package/plugins/agent-panel/client/describe.ts +57 -0
- package/plugins/agent-panel/client/driver-pick.ts +22 -0
- package/plugins/agent-panel/client/frame.ts +51 -0
- package/plugins/agent-panel/client/main.ts +343 -81
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +187 -61
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/desktop/frame.ts +231 -0
- package/plugins/desktop/launch.ts +44 -9
- package/plugins/desktop/main.ts +100 -32
- package/plugins/desktop/window.ts +21 -4
- package/plugins/driver-pty/client/terminal.ts +4 -1
- package/plugins/driver-pty/index.ts +38 -11
- package/plugins/driver-pty/spawn.ts +38 -0
- package/plugins/driver-pty/terminal.ts +2 -0
- package/plugins/extensions/index.ts +26 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/feedback/client.d.ts +22 -0
- package/plugins/feedback/client.js +471 -0
- package/plugins/feedback/index.ts +143 -0
- package/plugins/feedback/plugin.json +11 -0
- package/plugins/feedback/store.ts +307 -0
- package/plugins/home/index.ts +122 -0
- package/plugins/home/plugin.json +11 -0
- package/plugins/instance/index.ts +12 -3
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/shared/AGENTS.core.md +36 -16
- package/shared/skills/acryl-add-ui/SKILL.md +19 -0
- package/shared/skills/acryl-app/SKILL.md +13 -0
- package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
- package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
- package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
- package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
- package/shared/skills/cordis-plugin/SKILL.md +26 -0
- package/templates/agent/AGENTS.md +4 -2
- package/templates/agent/blend.yaml +4 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +150 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +73 -21
- package/templates/agent/template.json +2 -0
- package/templates/agent/test/app.test.ts +83 -5
- package/templates/blank/AGENTS.md +3 -2
- package/templates/blank/blend.yaml +11 -0
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +1 -0
- package/templates/blank/src/app.ts +100 -29
- package/templates/blank/src/door.ts +7 -0
- package/templates/blank/src/first-page.ts +5 -0
- package/templates/blank/src/frame.ts +17 -0
- package/templates/blank/src/routes.ts +65 -8
- package/templates/blank/template.json +14 -1
- package/templates/blank/test/app.test.ts +103 -16
- package/templates/chat/blend.yaml +2 -1
- package/templates/chat/gitignore +1 -0
- package/templates/chat/package.json +1 -0
- package/templates/chat/src/app.ts +72 -22
- package/templates/chat/src/chat-app.ts +11 -2
- package/templates/chat/src/client/chat.tsx +4 -2
- package/templates/chat/src/frame.ts +17 -0
- package/templates/chat/src/main.ts +1 -6
- package/templates/chat/src/page.tsx +6 -4
- package/templates/chat/src/routes.ts +14 -4
- package/templates/chat/src/runtime.ts +9 -3
- package/templates/chat/template.json +3 -1
- package/templates/chat/test/chat.test.ts +46 -1
- package/templates/chat/test/server.test.ts +17 -3
- package/templates/nodejs/blend.yaml +2 -1
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +1 -0
- package/templates/nodejs/src/app.ts +76 -21
- package/templates/nodejs/src/frame.ts +17 -0
- package/templates/nodejs/src/main.ts +1 -6
- package/templates/nodejs/src/page.tsx +3 -3
- package/templates/nodejs/src/routes.ts +32 -13
- package/templates/nodejs/template.json +3 -1
- package/templates/nodejs/test/app.test.ts +38 -3
- package/templates/nodejs/test/server.test.ts +17 -3
- package/templates/notes/blend.yaml +2 -1
- package/templates/notes/gitignore +1 -0
- package/templates/notes/package.json +1 -0
- package/templates/notes/src/app.ts +72 -22
- package/templates/notes/src/frame.ts +17 -0
- package/templates/notes/src/main.ts +1 -6
- package/templates/notes/src/notes-app.ts +24 -17
- package/templates/notes/src/routes.ts +11 -4
- package/templates/notes/src/runtime.ts +9 -3
- package/templates/notes/src/views.tsx +26 -17
- package/templates/notes/template.json +3 -1
- package/templates/notes/test/notes.test.ts +39 -1
- package/templates/notes/test/server.test.ts +17 -3
- package/templates/todo/blend.yaml +3 -0
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +1 -0
- package/templates/todo/src/app.ts +81 -27
- package/templates/todo/src/door.ts +7 -0
- package/templates/todo/src/frame.ts +17 -0
- package/templates/todo/src/page.ts +13 -6
- package/templates/todo/src/todo.ts +18 -6
- package/templates/todo/template.json +11 -1
- package/templates/todo/test/todo.test.ts +67 -9
- package/lib/src-LX0E4ec4.js +0 -117
- 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.
|
|
68
|
-
}, "acryl-router:
|
|
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.
|
|
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
|
-
/**
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
159
|
-
},
|
|
198
|
+
resolve(false);
|
|
199
|
+
}, ms);
|
|
160
200
|
});
|
|
161
|
-
const outcome = await Promise.race([Promise.allSettled(flights.map((flight) => flight.settled)).then(() =>
|
|
201
|
+
const outcome = await Promise.race([Promise.allSettled(flights.map((flight) => flight.settled)).then(() => true), gaveUp]);
|
|
162
202
|
clearTimeout(timer);
|
|
163
|
-
|
|
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", {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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. */
|