acrylblends 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.d.ts +47 -2
- package/dist/index.js +2017 -32
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +5 -3
- package/docs/framework/workshop/README.md +139 -153
- package/docs/framework/workshop/facilitator.md +69 -101
- 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-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +87 -9
- package/lib/cordis/door.js +324 -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.js +1 -1
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +10 -1
- package/lib/door/page.js +46 -7
- 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-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
- 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/plugin.json +3 -0
- package/plugins/acryl-door/src/cli/acryl.ts +63 -6
- package/plugins/acryl-door/src/host/channel.ts +86 -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 +128 -24
- package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
- 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 +14 -1
- package/plugins/acryl-door/src/index.ts +168 -30
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +3 -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 +32 -4
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +41 -2
- 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-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 +238 -26
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +146 -57
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/builder/index.ts +13 -35
- package/plugins/builder/package.ts +186 -0
- package/plugins/builder/plugin.json +3 -1
- 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 +47 -18
- package/plugins/extensions/plugin-store.ts +27 -0
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/extensions/stage.ts +15 -5
- 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 +6 -0
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/plugin.json +1 -1
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/plugins/plugin-share/README.md +51 -0
- package/plugins/plugin-share/index.ts +467 -0
- package/plugins/plugin-share/plugin.json +15 -0
- package/plugins/plugin-share/publish.ts +65 -0
- package/plugins/plugin-share/pull-check.ts +129 -0
- package/plugins/plugin-share/pulled-records.ts +70 -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/.gitattributes +2 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/blend.yaml +6 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +154 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +38 -19
- package/templates/agent/template.json +3 -0
- package/templates/agent/test/app.test.ts +45 -3
- package/templates/blank/.gitattributes +2 -0
- 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/.gitattributes +2 -0
- 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/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/.gitattributes +2 -0
- 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/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/.gitattributes +2 -0
- 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/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/.gitattributes +2 -0
- 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
|
@@ -5,7 +5,7 @@ import { r as streamSSE } from "./streaming-BlDaph8U.js";
|
|
|
5
5
|
import { i as loopbackOnly, r as loopbackAuthority } from "./loopback-only-BbasoF7R.js";
|
|
6
6
|
import "./helpers-BEjcdbgw.js";
|
|
7
7
|
import { d as parseChannelToHost, l as UiControlError, u as WINDOW_ID } from "./protocol-CQ_if7Dg.js";
|
|
8
|
-
import "./src-
|
|
8
|
+
import "./src-iBJ2ysV9.js";
|
|
9
9
|
import "./middleware-BfG_fp8P.js";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeSync } from "node:fs";
|
|
@@ -31,10 +31,12 @@ const DEFAULT_ACK_MS = 3e3;
|
|
|
31
31
|
/**
|
|
32
32
|
* The pages and who drives which. One page answers a call, chosen the same way every time: the page whose panel asked for the run that is making the call (`driveFor`), else the page the
|
|
33
33
|
* Host just sent somewhere (it comes back as a new page, `prefer`), else the one in front (it reports `focused`), else the most recently active; a page that did not acknowledge a call in time goes to the end of the line. An explicit desktop `window` picks its page.
|
|
34
|
+
* A call that names its `page` (the page a run was asked from) goes to that page alone, followed into its tab when it reloads or moves, and to no other.
|
|
34
35
|
*/
|
|
35
36
|
var UiChannel = class {
|
|
36
37
|
ackMs;
|
|
37
38
|
attachWaitMs;
|
|
39
|
+
requirePage;
|
|
38
40
|
/** The page the Host sent to a new place and that came back: it keeps being driven until the person focuses another page. */
|
|
39
41
|
preferred;
|
|
40
42
|
/** The pages whose panels have a run going, the latest last: a call with no `window` goes to the latest of them that is still connected. */
|
|
@@ -42,6 +44,7 @@ var UiChannel = class {
|
|
|
42
44
|
constructor(options = {}) {
|
|
43
45
|
this.ackMs = options.ackMs ?? DEFAULT_ACK_MS;
|
|
44
46
|
this.attachWaitMs = options.attachWaitMs ?? 0;
|
|
47
|
+
this.requirePage = options.requirePage === true;
|
|
45
48
|
}
|
|
46
49
|
connections = /* @__PURE__ */ new Map();
|
|
47
50
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -54,18 +57,21 @@ var UiChannel = class {
|
|
|
54
57
|
* Register a page's stream. A stream already open for the same window is closed first, and its calls settle as `disconnected`.
|
|
55
58
|
* @returns the function to call when this stream ends.
|
|
56
59
|
*/
|
|
57
|
-
attach(windowId, focused, stream, shellWindow) {
|
|
60
|
+
attach(windowId, focused, stream, shellWindow, tab) {
|
|
58
61
|
const previous = this.connections.get(windowId);
|
|
59
62
|
if (previous !== void 0) this.drop(previous, true);
|
|
60
63
|
const connection = {
|
|
61
64
|
stream,
|
|
62
65
|
windowId,
|
|
63
66
|
shellWindow,
|
|
67
|
+
tab,
|
|
64
68
|
focused,
|
|
65
69
|
lastActive: ++this.clock,
|
|
66
70
|
suspect: previous?.suspect === true
|
|
67
71
|
};
|
|
68
72
|
this.connections.set(windowId, connection);
|
|
73
|
+
for (const asker of this.askers) if (asker.windowId === windowId) asker.tab ??= tab;
|
|
74
|
+
else if (tab !== void 0 && asker.tab === tab) asker.windowId = windowId;
|
|
69
75
|
if (this.askers.some((asker) => asker.windowId === windowId)) this.tell(connection, "1");
|
|
70
76
|
for (const waiter of [...this.attachWaiters]) waiter(windowId);
|
|
71
77
|
return () => {
|
|
@@ -113,7 +119,11 @@ var UiChannel = class {
|
|
|
113
119
|
* @returns the function to call when the run ends.
|
|
114
120
|
*/
|
|
115
121
|
driveFor(windowId) {
|
|
116
|
-
const asker = {
|
|
122
|
+
const asker = {
|
|
123
|
+
origin: windowId,
|
|
124
|
+
windowId,
|
|
125
|
+
tab: this.connections.get(windowId)?.tab
|
|
126
|
+
};
|
|
117
127
|
this.askers.push(asker);
|
|
118
128
|
this.tellPage(windowId, "1");
|
|
119
129
|
return () => {
|
|
@@ -123,6 +133,46 @@ var UiChannel = class {
|
|
|
123
133
|
if (!this.askers.some((other) => other.windowId === asker.windowId)) this.tellPage(asker.windowId, "0");
|
|
124
134
|
};
|
|
125
135
|
}
|
|
136
|
+
/** Where the page a run was asked from is now: the same window, or the page that replaced it in the same tab. */
|
|
137
|
+
whereIs(page) {
|
|
138
|
+
const asker = [...this.askers].reverse().find((entry) => entry.origin === page);
|
|
139
|
+
return asker === void 0 ? {
|
|
140
|
+
windowId: page,
|
|
141
|
+
tab: this.connections.get(page)?.tab
|
|
142
|
+
} : {
|
|
143
|
+
windowId: asker.windowId,
|
|
144
|
+
tab: asker.tab
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* A promise for the run's page coming back after a reload or a move: the next page that connects in its tab (or with its window id). Ask for it BEFORE the call that makes the page
|
|
149
|
+
* leave. Pages of other tabs, another visitor's among them, are never taken for it.
|
|
150
|
+
* @param signal - stop waiting (the act did not move the page after all); the promise then answers `undefined`.
|
|
151
|
+
* @returns the window id of the page that came back, or `undefined` when it did not within `timeoutMs`.
|
|
152
|
+
*/
|
|
153
|
+
expectPageBack(page, timeoutMs, signal) {
|
|
154
|
+
const { windowId, tab } = this.whereIs(page);
|
|
155
|
+
return new Promise((resolve) => {
|
|
156
|
+
const done = (value) => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
this.attachWaiters.delete(waiter);
|
|
159
|
+
signal?.removeEventListener("abort", stop);
|
|
160
|
+
resolve(value);
|
|
161
|
+
};
|
|
162
|
+
const stop = () => {
|
|
163
|
+
done(void 0);
|
|
164
|
+
};
|
|
165
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
166
|
+
const waiter = (attached) => {
|
|
167
|
+
const connection = this.connections.get(attached);
|
|
168
|
+
if (attached === windowId || tab !== void 0 && connection?.tab === tab) done(attached);
|
|
169
|
+
};
|
|
170
|
+
const timer = setTimeout(() => {
|
|
171
|
+
done(void 0);
|
|
172
|
+
}, timeoutMs);
|
|
173
|
+
this.attachWaiters.add(waiter);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
126
176
|
tellPage(windowId, running) {
|
|
127
177
|
const connection = this.connections.get(windowId);
|
|
128
178
|
if (connection !== void 0) this.tell(connection, running);
|
|
@@ -138,7 +188,7 @@ var UiChannel = class {
|
|
|
138
188
|
if (connection === void 0) return;
|
|
139
189
|
this.preferred = windowId;
|
|
140
190
|
connection.lastActive = ++this.clock;
|
|
141
|
-
for (const asker of this.askers) if (!this.connections.has(asker.windowId)) asker.windowId = windowId;
|
|
191
|
+
for (const asker of this.askers) if (!this.connections.has(asker.windowId) && (asker.tab === void 0 || asker.tab === connection.tab)) asker.windowId = windowId;
|
|
142
192
|
if (this.askers.some((asker) => asker.windowId === windowId)) this.tellPage(windowId, "1");
|
|
143
193
|
}
|
|
144
194
|
/**
|
|
@@ -149,6 +199,8 @@ var UiChannel = class {
|
|
|
149
199
|
async call(request, options = {}, retried = false) {
|
|
150
200
|
if (this.closed) throw new UiControlError("unloaded", "the door was unloaded");
|
|
151
201
|
const readOnlyCall = request.op === "snapshot" || request.op === "wait";
|
|
202
|
+
if (options.page !== void 0) return await this.callPage(request, options.page, options, readOnlyCall);
|
|
203
|
+
if (this.requirePage) throw new UiControlError("no-window", "this door drives only the page whose visitor asked, and the call names no page");
|
|
152
204
|
if (readOnlyCall && this.attachWaitMs > 0 && this.connections.size === 0) await this.expectNewPage(this.attachWaitMs);
|
|
153
205
|
const ordered = this.ordered();
|
|
154
206
|
const candidates = options.window === void 0 ? ordered : ordered.filter((connection) => connection.shellWindow === options.window);
|
|
@@ -174,6 +226,25 @@ var UiChannel = class {
|
|
|
174
226
|
}
|
|
175
227
|
throw new UiControlError("timeout", `no page of the app answered within ${String(Math.round(this.ackMs / 1e3))} s: the app's pages are open but none responded. Likely causes: a tab in the background, a page that is still loading after a navigation (look again), a page busy running a long script, or a page that holds the browser's six connections with event streams of its own (a page should keep to two). Bring the app's tab to the front, or navigate it, then look again`);
|
|
176
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* A call for one page (the page a run was asked from, followed into its tab): it goes to that page alone and never fails over to another one. A page that is between two
|
|
231
|
+
* documents (it reloads, or moves to another path) is waited for, up to the attach wait.
|
|
232
|
+
*/
|
|
233
|
+
async callPage(request, page, options, readOnly) {
|
|
234
|
+
let target = this.connections.get(this.whereIs(page).windowId);
|
|
235
|
+
if (target === void 0 && this.attachWaitMs > 0) {
|
|
236
|
+
const back = await this.expectPageBack(page, this.attachWaitMs);
|
|
237
|
+
if (back !== void 0) target = this.connections.get(back);
|
|
238
|
+
}
|
|
239
|
+
if (target === void 0) throw new UiControlError("no-window", "the page this run is for is not open any more");
|
|
240
|
+
if (options.signal?.aborted === true) throw new UiControlError("aborted", "the call was cancelled");
|
|
241
|
+
try {
|
|
242
|
+
return await this.callOne(target, request, options, readOnly);
|
|
243
|
+
} catch (cause) {
|
|
244
|
+
if (cause instanceof Silent) throw new UiControlError("timeout", "the page this run is for did not answer: is its tab in the background or still loading? Look again");
|
|
245
|
+
throw cause;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
177
248
|
callOne(target, request, options, failOver) {
|
|
178
249
|
const signal = options.signal;
|
|
179
250
|
const id = this.nextId;
|
|
@@ -239,16 +310,22 @@ var UiChannel = class {
|
|
|
239
310
|
}
|
|
240
311
|
/**
|
|
241
312
|
* A promise for the window id of the next page that connects and was not connected when this was asked (the page that came back after a reload or a move).
|
|
313
|
+
* @param signal - stop waiting; the promise then answers `undefined`.
|
|
242
314
|
* @returns that id, or `undefined` when no new page connected within `timeoutMs`.
|
|
243
315
|
*/
|
|
244
|
-
expectNewPage(timeoutMs) {
|
|
316
|
+
expectNewPage(timeoutMs, signal) {
|
|
245
317
|
const known = new Set(this.connections.keys());
|
|
246
318
|
return new Promise((resolve) => {
|
|
247
319
|
const done = (value) => {
|
|
248
320
|
clearTimeout(timer);
|
|
249
321
|
this.attachWaiters.delete(waiter);
|
|
322
|
+
signal?.removeEventListener("abort", stop);
|
|
250
323
|
resolve(value);
|
|
251
324
|
};
|
|
325
|
+
const stop = () => {
|
|
326
|
+
done(void 0);
|
|
327
|
+
};
|
|
328
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
252
329
|
const waiter = (windowId) => {
|
|
253
330
|
if (!known.has(windowId)) done(windowId);
|
|
254
331
|
};
|
|
@@ -454,6 +531,8 @@ function pageChannelRoutes(channel, options) {
|
|
|
454
531
|
const focused = c.req.query("focused") === "1";
|
|
455
532
|
const shellQuery = Number(c.req.query("shell"));
|
|
456
533
|
const shellWindow = Number.isSafeInteger(shellQuery) && shellQuery > 0 ? shellQuery : void 0;
|
|
534
|
+
const tabQuery = c.req.query("tab");
|
|
535
|
+
const tab = tabQuery !== void 0 && WINDOW_ID.test(tabQuery) ? tabQuery : void 0;
|
|
457
536
|
const signal = c.req.raw.signal;
|
|
458
537
|
return streamSSE(c, async (stream) => {
|
|
459
538
|
let end = () => {};
|
|
@@ -474,7 +553,7 @@ function pageChannelRoutes(channel, options) {
|
|
|
474
553
|
},
|
|
475
554
|
notify: write,
|
|
476
555
|
close: end
|
|
477
|
-
}, shellWindow);
|
|
556
|
+
}, shellWindow, tab);
|
|
478
557
|
const beat = setInterval(() => {
|
|
479
558
|
write("ping", "");
|
|
480
559
|
}, heartbeatMs);
|
|
@@ -508,7 +587,8 @@ function renderSnapshot(snapshot, more = (cursor) => `call ui_snapshot with curs
|
|
|
508
587
|
const level = node.level === void 0 ? "" : ` level=${String(node.level)}`;
|
|
509
588
|
const states = node.states.length === 0 ? "" : ` [${node.states.join(", ")}]`;
|
|
510
589
|
const value = node.value === void 0 ? "" : ` value=${JSON.stringify(node.value)}`;
|
|
511
|
-
|
|
590
|
+
const options = node.options === void 0 ? "" : ` options=${JSON.stringify(node.options)}`;
|
|
591
|
+
return `${" ".repeat(Math.min(node.depth, 8))}- ${node.role}${node.name === "" ? "" : ` ${JSON.stringify(node.name)}`}${level}${states}${value}${options} [ref=${node.ref}]`;
|
|
512
592
|
});
|
|
513
593
|
const rest = snapshot.nextCursor === void 0 ? "" : `\n(${String(snapshot.total - snapshot.nextCursor)} more elements; ${more(snapshot.nextCursor)})`;
|
|
514
594
|
return `Page: ${snapshot.title || "(untitled)"}\nSnapshot ${String(snapshot.generation)}, ${String(snapshot.nodes.length)} of ${String(snapshot.total)} elements:\n${lines.join("\n")}${rest}`;
|
|
@@ -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 };
|