acrylblends 0.10.0 → 0.11.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/docs/framework/cordis/node-server.md +25 -3
- package/lib/bun.d.ts +1 -1
- package/lib/cloudflare-workers.d.ts +1 -1
- package/lib/cordis/door.d.ts +2 -1
- package/lib/cordis/door.js +7 -4
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +14 -2
- package/lib/cordis/node-server.js +27 -3
- package/lib/cordis/router-http.d.ts +25 -2
- package/lib/cordis/router-http.js +54 -6
- 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.js +1 -1
- package/lib/{index-BvFh1Gfx.d.ts → index-C-SklY6u.d.ts} +3 -0
- package/lib/{index-BqVQkTnu.d.ts → index-D4nVR3wu.d.ts} +18 -3
- package/lib/node-server.d.ts +2 -38
- package/lib/node-server.js +2 -2
- package/lib/{snapshot-text-D3NspDyT.js → snapshot-text-yaWlezbd.js} +1 -1
- package/lib/{src-CwABFr6r.js → src-DNzw0Qsb.js} +3 -2
- package/lib/test-websocket-DyeS50Tr.d.ts +136 -0
- package/lib/{server-DvgMGtzo.js → test-websocket-ZXHNHQSU.js} +225 -3
- package/lib/ws.d.ts +1 -1
- package/lib/ws.js +3 -0
- package/package.json +6 -2
- package/plugins/acryl-door/src/host/door.ts +5 -3
- package/plugins/acryl-door/src/index.ts +1 -1
- package/plugins/acryl-node-server/plugin.json +1 -1
- package/plugins/acryl-node-server/src/adapter/websocket-types.ts +5 -1
- package/plugins/acryl-node-server/src/adapter/websocket.ts +33 -5
- package/plugins/acryl-node-server/src/index.ts +35 -1
- package/plugins/acryl-node-server/src/test-websocket.ts +110 -0
- package/plugins/acryl-node-server/src/upgrade-hub.ts +152 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.ts +5 -0
- package/plugins/acryl-router/src/service.ts +18 -4
- package/plugins/acryl-router-http/plugin.json +2 -2
- package/plugins/acryl-router-http/src/index.ts +70 -6
- package/shared/AGENTS.core.md +1 -1
- package/shared/DEFAULTS.md +1 -1
- package/lib/error-t4N2js2I.d.ts +0 -8
|
@@ -72,7 +72,29 @@ plugin that only mounted routes is different: its mount and the requests it is s
|
|
|
72
72
|
Let the process configuration choose the address. Do not hard-code a development port in a reusable plugin: a plugin mounts routes and
|
|
73
73
|
leaves the socket to whichever adapter the app runs with.
|
|
74
74
|
|
|
75
|
-
##
|
|
75
|
+
## WebSocket upgrades
|
|
76
76
|
|
|
77
|
-
WebSocket upgrades
|
|
78
|
-
|
|
77
|
+
A plugin takes WebSocket upgrades with `ctx.nodeServer.upgrades()` (or `ctx.routerHttp.upgrades()` on the Host's web server, with `ctx.routerHttp.mount(prefix, { upgrade: [paths] })` naming the exact paths). It returns the ported `upgradeWebSocket`; the route is an ordinary route with ordinary middleware in front of it:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
export function apply(ctx: Context) {
|
|
81
|
+
const upgrade = ctx.nodeServer.upgrades()
|
|
82
|
+
ctx.router.route('/', new Acryl()
|
|
83
|
+
// Checks run before the upgrade: own page, loopback peer, exact Origin and Host. A refusal is a plain HTTP status with Connection: close, sent before any handshake.
|
|
84
|
+
.use('/terminal', ownPage)
|
|
85
|
+
.get('/terminal', upgrade(() => ({
|
|
86
|
+
onMessage(event, socket) {
|
|
87
|
+
if (socket.bufferedAmount < 1_000_000) socket.send(`echo ${String(event.data)}`)
|
|
88
|
+
},
|
|
89
|
+
onClose(event) {
|
|
90
|
+
// Called exactly once, whoever closed the socket.
|
|
91
|
+
},
|
|
92
|
+
}), { textOnly: true })))
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- **Limits.** Text frames only with `{ textOnly: true }` (binary frames never reach `onMessage`); `websocket.maxPayload` (default 1 MiB, a bigger frame closes the socket with 1009); no per-message compression; `socket.bufferedAmount` for back-pressure; `socket.close(code, reason)` with application codes such as 1013 or 4404 (a code the protocol forbids on the wire becomes 1011). No raw socket is exposed.
|
|
97
|
+
- **Dead peers.** The server pings every `websocket.heartbeat` ms (default 30 000) and terminates a peer that did not answer the last ping. `websocket.grace` (default 2000) is how long a closing socket gets.
|
|
98
|
+
- **Ownership.** The sockets a plugin's routes accepted belong to that plugin. When it unloads they are closed with 1001 within the grace (what is left is terminated), its handler sees `close` once, other plugins' sockets are untouched, and `ctx.nodeServer.sockets` counts what is still open. Stopping the server closes every socket.
|
|
99
|
+
- **Needs `ws`.** `npm install ws`; an app that never upgrades does not need it.
|
|
100
|
+
- **Tests.** `testWebSocket((request, env) => app.fetch(request, env), '/terminal', { headers })` from `acrylblends/node-server` runs the route and its middleware in process: a refusal answers `{ status }`, an accepted connection gives a `socket` that sends text frames, collects what the handler sent (`received`) and closes.
|
package/lib/bun.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { L as MiddlewareHandler, t as Context, v as Env } from "./context-DS3fYyG-.js";
|
|
2
2
|
import { r as GetConnInfo } from "./types-Dv_nV2cv.js";
|
|
3
3
|
import { f as FileSystemModule, m as ToSSGAdaptorInterface } from "./index-DaJtL7ci.js";
|
|
4
|
-
import { a as WSEvents, n as UpgradeWebSocket } from "./index-
|
|
4
|
+
import { a as WSEvents, n as UpgradeWebSocket } from "./index-C-SklY6u.js";
|
|
5
5
|
import { t as ServeStaticOptions } from "./index-CseujaYI.js";
|
|
6
6
|
|
|
7
7
|
//#region ../../plugins/acryl-router/src/core/adapter/bun/serve-static.d.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { L as MiddlewareHandler, v as Env } from "./context-DS3fYyG-.js";
|
|
2
2
|
import { r as GetConnInfo } from "./types-Dv_nV2cv.js";
|
|
3
|
-
import { a as WSEvents, n as UpgradeWebSocket } from "./index-
|
|
3
|
+
import { a as WSEvents, n as UpgradeWebSocket } from "./index-C-SklY6u.js";
|
|
4
4
|
import { t as ServeStaticOptions$1 } from "./index-CseujaYI.js";
|
|
5
5
|
|
|
6
6
|
//#region ../../plugins/acryl-router/src/core/adapter/cloudflare-workers/serve-static.d.ts
|
package/lib/cordis/door.d.ts
CHANGED
|
@@ -32,10 +32,11 @@ interface ToolsPort {
|
|
|
32
32
|
interface DoorDeps {
|
|
33
33
|
readonly channel: UiChannel;
|
|
34
34
|
readonly audit: AuditLog;
|
|
35
|
-
/** The app's name and
|
|
35
|
+
/** The app's name, folder and, when it listens, its address (what `brief` prints so an agent can curl the running app). */
|
|
36
36
|
readonly app: {
|
|
37
37
|
readonly name: string;
|
|
38
38
|
readonly root: string;
|
|
39
|
+
readonly url?: string | undefined;
|
|
39
40
|
};
|
|
40
41
|
/** Read at each call: the services may come and go while the door runs. */
|
|
41
42
|
readonly extensions: () => ExtensionsPort | undefined;
|
package/lib/cordis/door.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as Acryl } from "../app-BM0rTCp7.js";
|
|
2
2
|
import { n as clientModules } from "../client-modules-CLvWOjbd.js";
|
|
3
3
|
import { MAX_SNAPSHOT_NODES, MAX_WAIT_MS, UiControlError, parseUiRequest } from "../door/protocol.js";
|
|
4
|
-
import { a as INVALID_JSON, c as operatorOnly, d as AuditLog, f as auditPath, i as DOOR_PREFIX, l as privateApi, o as error, p as UiChannel, r as pageChannelRoutes, s as jsonBody, t as renderSnapshot, u as readJson } from "../snapshot-text-
|
|
5
|
-
import "../src-
|
|
4
|
+
import { a as INVALID_JSON, c as operatorOnly, d as AuditLog, f as auditPath, i as DOOR_PREFIX, l as privateApi, o as error, p as UiChannel, r as pageChannelRoutes, s as jsonBody, t as renderSnapshot, u as readJson } from "../snapshot-text-yaWlezbd.js";
|
|
5
|
+
import "../src-DNzw0Qsb.js";
|
|
6
6
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -255,11 +255,12 @@ var Door = class {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
brief() {
|
|
258
|
-
const { name, root } = this.deps.app;
|
|
258
|
+
const { name, root, url } = this.deps.app;
|
|
259
259
|
const read = SELF_KNOWLEDGE.filter((path) => existsSync(resolve(root, path)));
|
|
260
260
|
const value = {
|
|
261
261
|
app: name,
|
|
262
262
|
root,
|
|
263
|
+
...url === void 0 ? {} : { url },
|
|
263
264
|
read,
|
|
264
265
|
page: this.deps.channel.connected ? "open" : "not open",
|
|
265
266
|
plugins: this.deps.extensions() === void 0 ? "unavailable" : "available",
|
|
@@ -269,6 +270,7 @@ var Door = class {
|
|
|
269
270
|
value,
|
|
270
271
|
text: [
|
|
271
272
|
`App ${name} in ${root}.`,
|
|
273
|
+
...url === void 0 ? [] : [`Address: ${url} (the running app; curl it to check a route).`],
|
|
272
274
|
read.length === 0 ? "No AGENTS.md or skill in the folder." : `Read first: ${read.join(", ")}.`,
|
|
273
275
|
`Page: ${value.page}${value.page === "not open" ? " (open the app in a browser to look and act)" : ""}.`,
|
|
274
276
|
`Live plugins: ${value.plugins}.`,
|
|
@@ -730,7 +732,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
730
732
|
audit: new AuditLog(auditPath(stateDir(app.root))),
|
|
731
733
|
app: {
|
|
732
734
|
name: app.name,
|
|
733
|
-
root: app.root
|
|
735
|
+
root: app.root,
|
|
736
|
+
url: loopbackUrl(server.url)
|
|
734
737
|
},
|
|
735
738
|
extensions: () => ctx.get("extensions"),
|
|
736
739
|
tools: () => ctx.get("tools"),
|
package/lib/cordis/inspect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as Acryl } from "../app-BM0rTCp7.js";
|
|
2
2
|
import { loopbackOnly } from "../loopback-only.js";
|
|
3
|
-
import "../src-
|
|
3
|
+
import "../src-DNzw0Qsb.js";
|
|
4
4
|
import "../middleware-CPQ8himN.js";
|
|
5
5
|
import { Service } from "@deepseek-ai/cordis";
|
|
6
6
|
//#region ../../plugins/acryl-inspect/src/report.ts
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { t as RequestError } from "../
|
|
1
|
+
import { c as UpgradeHubOptions, i as testWebSocket, l as UpgradeRouter, n as TestWebSocketOptions, o as UpgradeEmitter, p as WebSocketLike, r as TestWebSocketResult, s as UpgradeHub, t as TestSocket, u as RequestError } from "../test-websocket-DyeS50Tr.js";
|
|
2
|
+
import { n as UpgradeWebSocket } from "../index-C-SklY6u.js";
|
|
2
3
|
import { ServeStaticOptions, serveStatic } from "../node-server/serve-static.js";
|
|
3
4
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
4
5
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -11,6 +12,8 @@ interface Config {
|
|
|
11
12
|
hostname: string;
|
|
12
13
|
port: number;
|
|
13
14
|
scan: number;
|
|
15
|
+
/** Limits of WebSocket upgrades (`upgrades()`); the defaults suit most apps. */
|
|
16
|
+
websocket: UpgradeHubOptions;
|
|
14
17
|
}
|
|
15
18
|
/** Every field has a default, so a caller passes only what it changes: `ctx.plugin(plugin, { ... })` takes the partial shape. */
|
|
16
19
|
declare const Config: z<Partial<Config>, Config>;
|
|
@@ -23,8 +26,17 @@ declare class NodeServer extends Service {
|
|
|
23
26
|
readonly hostname: string;
|
|
24
27
|
readonly config: Config;
|
|
25
28
|
private readonly server;
|
|
29
|
+
private hub;
|
|
26
30
|
private bound;
|
|
27
31
|
constructor(ctx: Context, config: Config);
|
|
32
|
+
/**
|
|
33
|
+
* `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
|
|
34
|
+
* 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
|
|
35
|
+
* 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.
|
|
36
|
+
*/
|
|
37
|
+
upgrades(): UpgradeWebSocket<WebSocketLike>;
|
|
38
|
+
/** How many WebSocket connections are open now. */
|
|
39
|
+
get sockets(): number;
|
|
28
40
|
/** The port in use, 0 before the server listens. */
|
|
29
41
|
get port(): number;
|
|
30
42
|
/** The address clients use. */
|
|
@@ -42,4 +54,4 @@ declare function isPortUnavailable(error: unknown, platform?: NodeJS.Platform):
|
|
|
42
54
|
/** Loads once the server listens, so a plugin that injects `nodeServer` can rely on `url`. A failure to bind fails the plugin. */
|
|
43
55
|
declare function apply(ctx: Context, config?: Partial<Config>): Promise<void>;
|
|
44
56
|
//#endregion
|
|
45
|
-
export { Config, NodeServer, RequestError, type ServeStaticOptions, apply, inject, isPortUnavailable, name, serveStatic };
|
|
57
|
+
export { Config, NodeServer, RequestError, type ServeStaticOptions, type TestSocket, type TestWebSocketOptions, type TestWebSocketResult, UpgradeEmitter, UpgradeHub, type UpgradeHubOptions, type UpgradeRouter, apply, inject, isPortUnavailable, name, serveStatic, testWebSocket };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as Request, i as createAdaptorServer, l as RequestError, n as UpgradeEmitter, r as UpgradeHub, t as testWebSocket } from "../test-websocket-ZXHNHQSU.js";
|
|
2
2
|
import { t as serveStatic } from "../serve-static-3uo_9rjF.js";
|
|
3
3
|
import { Service } from "@deepseek-ai/cordis";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -10,12 +10,18 @@ const inject = ["router"];
|
|
|
10
10
|
const Config = z.object({
|
|
11
11
|
hostname: z.string().default("127.0.0.1").description("Address to bind. Loopback by default; binding elsewhere exposes the app to the network."),
|
|
12
12
|
port: z.number().min(0).max(65535).default(0).description("Port to listen on; 0 lets the system choose. A taken port is followed by the next free one."),
|
|
13
|
-
scan: z.number().min(0).default(20).description("How many further ports to try when the configured one is taken.")
|
|
13
|
+
scan: z.number().min(0).default(20).description("How many further ports to try when the configured one is taken."),
|
|
14
|
+
websocket: z.object({
|
|
15
|
+
maxPayload: z.number().min(1).default(1024 * 1024).description("The largest WebSocket frame accepted, in bytes; a bigger one closes the socket with 1009."),
|
|
16
|
+
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."),
|
|
17
|
+
grace: z.number().min(0).default(2e3).description("Milliseconds a closing socket gets before it is terminated when its plugin unloads.")
|
|
18
|
+
}).default({})
|
|
14
19
|
});
|
|
15
20
|
var NodeServer = class extends Service {
|
|
16
21
|
hostname;
|
|
17
22
|
config;
|
|
18
23
|
server;
|
|
24
|
+
hub;
|
|
19
25
|
bound = 0;
|
|
20
26
|
constructor(ctx, config) {
|
|
21
27
|
super(ctx, "nodeServer");
|
|
@@ -31,6 +37,7 @@ var NodeServer = class extends Service {
|
|
|
31
37
|
overrideGlobalObjects: false
|
|
32
38
|
});
|
|
33
39
|
ctx.effect(() => async () => {
|
|
40
|
+
await this.hub?.close();
|
|
34
41
|
this.server.closeAllConnections();
|
|
35
42
|
if (this.server.listening) await new Promise((resolve) => {
|
|
36
43
|
this.server.close(() => {
|
|
@@ -39,6 +46,22 @@ var NodeServer = class extends Service {
|
|
|
39
46
|
});
|
|
40
47
|
}, "acryl-node-server: close listener and open connections");
|
|
41
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* `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
|
|
51
|
+
* 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
|
|
52
|
+
* 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.
|
|
53
|
+
*/
|
|
54
|
+
upgrades() {
|
|
55
|
+
if (!this.hub) {
|
|
56
|
+
this.hub = new UpgradeHub(this.config.websocket);
|
|
57
|
+
this.hub.attach(this.server, this.ctx.router);
|
|
58
|
+
}
|
|
59
|
+
return this.hub.upgradeWebSocketFor(this.ctx);
|
|
60
|
+
}
|
|
61
|
+
/** How many WebSocket connections are open now. */
|
|
62
|
+
get sockets() {
|
|
63
|
+
return this.hub?.size ?? 0;
|
|
64
|
+
}
|
|
42
65
|
/** The port in use, 0 before the server listens. */
|
|
43
66
|
get port() {
|
|
44
67
|
return this.bound;
|
|
@@ -86,8 +109,9 @@ async function apply(ctx, config = {}) {
|
|
|
86
109
|
hostname: "127.0.0.1",
|
|
87
110
|
port: 0,
|
|
88
111
|
scan: 20,
|
|
112
|
+
websocket: {},
|
|
89
113
|
...config
|
|
90
114
|
}).listen();
|
|
91
115
|
}
|
|
92
116
|
//#endregion
|
|
93
|
-
export { Config, NodeServer, RequestError, apply, inject, isPortUnavailable, name, serveStatic };
|
|
117
|
+
export { Config, NodeServer, RequestError, UpgradeEmitter, UpgradeHub, apply, inject, isPortUnavailable, name, serveStatic, testWebSocket };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as UpgradeWebSocket } from "../index-C-SklY6u.js";
|
|
2
|
+
import { i as RouterHttpService } from "../index-D4nVR3wu.js";
|
|
2
3
|
import { IncomingMessage, ServerResponse } from "node:http";
|
|
4
|
+
import { Duplex } from "node:stream";
|
|
3
5
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
4
6
|
|
|
5
7
|
//#region ../../plugins/acryl-router-http/src/serve.d.ts
|
|
@@ -40,16 +42,37 @@ interface WebServerPort {
|
|
|
40
42
|
readonly path: string;
|
|
41
43
|
readonly handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
|
42
44
|
}): () => void;
|
|
45
|
+
/** One exact-path upgrade claim (`WebUpgradeRoute` of the web server); optional because an older web server has none. */
|
|
46
|
+
registerUpgrade?(route: {
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly handler: (req: IncomingMessage, socket: Duplex, head: Buffer) => void | Promise<void>;
|
|
49
|
+
}): () => void;
|
|
43
50
|
}
|
|
44
51
|
declare class RouterHttp extends Service implements RouterHttpService {
|
|
45
52
|
private readonly router;
|
|
46
53
|
private readonly web;
|
|
54
|
+
private hub;
|
|
55
|
+
/** The adapter's own context: the hub lives as long as the adapter, whichever plugin asked for it first (`this.ctx` is the caller's). */
|
|
56
|
+
private readonly home;
|
|
57
|
+
private readonly upgradesIn;
|
|
47
58
|
constructor(ctx: Context);
|
|
48
59
|
/**
|
|
49
60
|
* Serve `prefix` and everything below it through the router. Returns a remover; the mount also goes away when the
|
|
50
61
|
* calling plugin unloads. A prefix another route already claims throws, as the webServer treats it as misconfiguration.
|
|
62
|
+
* `options.upgrade` names the exact paths that may be upgraded to a WebSocket: each must be inside the prefix, and each is one
|
|
63
|
+
* exact-path upgrade route of the web server, removed with the mount. No other path of the mount accepts an upgrade.
|
|
64
|
+
*/
|
|
65
|
+
mount(prefix: string, options?: {
|
|
66
|
+
readonly upgrade?: readonly string[];
|
|
67
|
+
}): () => void;
|
|
68
|
+
/**
|
|
69
|
+
* `upgradeWebSocket` for the calling plugin: the sockets its routes accept are closed with 1001 when it unloads. Needs the `ws` package.
|
|
70
|
+
* The route must be on a path the plugin mounted with `upgrade: [path]`.
|
|
51
71
|
*/
|
|
52
|
-
|
|
72
|
+
upgrades(): UpgradeWebSocket;
|
|
73
|
+
/** How many WebSocket connections are open now. */
|
|
74
|
+
get sockets(): number;
|
|
75
|
+
private ensureHub;
|
|
53
76
|
}
|
|
54
77
|
declare function apply(ctx: Context): void;
|
|
55
78
|
//#endregion
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { n as UpgradeEmitter, r as UpgradeHub } from "../test-websocket-ZXHNHQSU.js";
|
|
2
|
+
import "./node-server.js";
|
|
1
3
|
import { pipeline } from "node:stream/promises";
|
|
2
4
|
import { Readable } from "node:stream";
|
|
3
5
|
import { Service } from "@deepseek-ai/cordis";
|
|
@@ -88,27 +90,73 @@ const PREFIX_PATTERN = /^\/[A-Za-z0-9._~\-/]*[A-Za-z0-9._~-]$/;
|
|
|
88
90
|
var RouterHttp = class extends Service {
|
|
89
91
|
router;
|
|
90
92
|
web;
|
|
93
|
+
hub;
|
|
94
|
+
/** The adapter's own context: the hub lives as long as the adapter, whichever plugin asked for it first (`this.ctx` is the caller's). */
|
|
95
|
+
home;
|
|
96
|
+
upgradesIn = new UpgradeEmitter();
|
|
91
97
|
constructor(ctx) {
|
|
92
98
|
super(ctx, "routerHttp");
|
|
99
|
+
this.home = ctx;
|
|
93
100
|
this.router = ctx.router;
|
|
94
101
|
this.web = ctx.get("webServer");
|
|
95
102
|
}
|
|
96
103
|
/**
|
|
97
104
|
* Serve `prefix` and everything below it through the router. Returns a remover; the mount also goes away when the
|
|
98
105
|
* calling plugin unloads. A prefix another route already claims throws, as the webServer treats it as misconfiguration.
|
|
106
|
+
* `options.upgrade` names the exact paths that may be upgraded to a WebSocket: each must be inside the prefix, and each is one
|
|
107
|
+
* exact-path upgrade route of the web server, removed with the mount. No other path of the mount accepts an upgrade.
|
|
99
108
|
*/
|
|
100
|
-
mount(prefix) {
|
|
109
|
+
mount(prefix, options = {}) {
|
|
101
110
|
if (!PREFIX_PATTERN.test(prefix)) throw new TypeError(`acryl-router-http: "${prefix}" is not a mountable path prefix`);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
const upgrade = options.upgrade ?? [];
|
|
112
|
+
for (const path of upgrade) if (!PREFIX_PATTERN.test(path) || !(path === prefix || path.startsWith(`${prefix}/`))) throw new TypeError(`acryl-router-http: the upgrade path "${path}" is not inside the prefix "${prefix}"`);
|
|
113
|
+
if (upgrade.length > 0 && this.web.registerUpgrade === void 0) throw new TypeError("acryl-router-http: this web server cannot take WebSocket upgrades (it has no registerUpgrade)");
|
|
114
|
+
const removers = [];
|
|
115
|
+
try {
|
|
116
|
+
if (upgrade.length > 0) this.ensureHub();
|
|
117
|
+
for (const path of upgrade) removers.push(this.web.registerUpgrade({
|
|
118
|
+
path,
|
|
119
|
+
handler: (req, socket, head) => {
|
|
120
|
+
this.upgradesIn.emit("upgrade", req, socket, head);
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
removers.push(this.web.register({
|
|
124
|
+
kind: "prefix",
|
|
125
|
+
path: prefix,
|
|
126
|
+
handler: (req, res) => serve(this.router, req, res)
|
|
127
|
+
}));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
for (const remove of removers) remove();
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
const remove = () => {
|
|
133
|
+
for (const undo of removers.splice(0)) undo();
|
|
134
|
+
};
|
|
107
135
|
this.ctx.effect(() => () => {
|
|
108
136
|
remove();
|
|
109
137
|
}, `acryl-router-http: mount ${prefix}`);
|
|
110
138
|
return remove;
|
|
111
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* `upgradeWebSocket` for the calling plugin: the sockets its routes accept are closed with 1001 when it unloads. Needs the `ws` package.
|
|
142
|
+
* The route must be on a path the plugin mounted with `upgrade: [path]`.
|
|
143
|
+
*/
|
|
144
|
+
upgrades() {
|
|
145
|
+
return this.ensureHub().upgradeWebSocketFor(this.ctx);
|
|
146
|
+
}
|
|
147
|
+
/** How many WebSocket connections are open now. */
|
|
148
|
+
get sockets() {
|
|
149
|
+
return this.hub?.size ?? 0;
|
|
150
|
+
}
|
|
151
|
+
ensureHub() {
|
|
152
|
+
if (!this.hub) {
|
|
153
|
+
const hub = new UpgradeHub();
|
|
154
|
+
hub.attach(this.upgradesIn, this.router);
|
|
155
|
+
this.hub = hub;
|
|
156
|
+
this.home.effect(() => () => hub.close(), "acryl-router-http: WebSocket hub");
|
|
157
|
+
}
|
|
158
|
+
return this.hub;
|
|
159
|
+
}
|
|
112
160
|
};
|
|
113
161
|
function apply(ctx) {
|
|
114
162
|
new RouterHttp(ctx);
|
package/lib/cordis/router.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ import { c as StatusCode, n as ContentfulStatusCode } from "../http-status-DaAwG
|
|
|
3
3
|
import { a as getConnInfo, i as connectionOf, n as RouterBindings, r as RouterEnv, t as ConnectionInfo } from "../connection-ZLEso7XL.js";
|
|
4
4
|
import { t as Acryl } from "../app-D8riatvv.js";
|
|
5
5
|
import { n as AppRequest, t as App } from "../deprecated-CTy4N7ng.js";
|
|
6
|
-
import { a as TrailingSlash, i as RouterHttpService, n as MountableApp, o as apply, r as Router, s as name, t as Config } from "../index-
|
|
6
|
+
import { a as TrailingSlash, i as RouterHttpService, n as MountableApp, o as apply, r as Router, s as name, t as Config } from "../index-D4nVR3wu.js";
|
|
7
7
|
import { t as HTTPException } from "../http-exception-kpGJkmpi.js";
|
|
8
8
|
export { Acryl, type AcrylRequest, App, type AppRequest, Config, type ConnectionInfo, type ContentfulStatusCode, Context, type ContextRenderer, type ContextVariableMap, type Env, type ErrorHandler, type ExecutionContext, HTTPException, type Handler, type Input, type MiddlewareHandler, type MountableApp, type Next, type NotFoundHandler, type NotFoundResponse, Router, type RouterBindings, type RouterEnv, type RouterHttpService, type Schema, type StatusCode, type ToSchema, type TrailingSlash, type TypedResponse, type ValidationTargets, apply, connectionOf, getConnInfo, name };
|
package/lib/cordis/router.js
CHANGED
|
@@ -3,5 +3,5 @@ import { t as Context } from "../context-DDO0cfuP.js";
|
|
|
3
3
|
import { t as Acryl } from "../app-BM0rTCp7.js";
|
|
4
4
|
import { t as App } from "../deprecated-BP4fPt38.js";
|
|
5
5
|
import { n as getConnInfo, t as connectionOf } from "../connection-C7xgP1hP.js";
|
|
6
|
-
import { i as name, n as Router, r as apply, t as Config } from "../src-
|
|
6
|
+
import { i as name, n as Router, r as apply, t as Config } from "../src-DNzw0Qsb.js";
|
|
7
7
|
export { Acryl, App, Config, Context, HTTPException, Router, apply, connectionOf, getConnInfo, name };
|
package/lib/cordis/state.js
CHANGED
|
@@ -3,7 +3,7 @@ import { methodNotAllowed } from "../method-not-allowed.js";
|
|
|
3
3
|
import { r as streamSSE } from "../streaming-DHST3Scg.js";
|
|
4
4
|
import { loopbackOnly } from "../loopback-only.js";
|
|
5
5
|
import "../helpers-CvTauQT5.js";
|
|
6
|
-
import "../src-
|
|
6
|
+
import "../src-DNzw0Qsb.js";
|
|
7
7
|
import "../middleware-CPQ8himN.js";
|
|
8
8
|
import { Service } from "@deepseek-ai/cordis";
|
|
9
9
|
//#region ../../plugins/acryl-state/src/cell.ts
|
package/lib/door/host.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { d as AuditLog, f as auditPath, n as HEARTBEAT_MS, p as UiChannel, r as pageChannelRoutes, t as renderSnapshot } from "../snapshot-text-
|
|
1
|
+
import { d as AuditLog, f as auditPath, n as HEARTBEAT_MS, p as UiChannel, r as pageChannelRoutes, t as renderSnapshot } from "../snapshot-text-yaWlezbd.js";
|
|
2
2
|
export { AuditLog, HEARTBEAT_MS, UiChannel, auditPath, pageChannelRoutes, renderSnapshot };
|
|
@@ -33,6 +33,8 @@ interface WSContextInit<T = unknown> {
|
|
|
33
33
|
close(code?: number, reason?: string): void;
|
|
34
34
|
raw?: T;
|
|
35
35
|
readyState: WSReadyState;
|
|
36
|
+
/** Bytes queued to send and not yet written to the socket; a sender that cares about back-pressure reads it. */
|
|
37
|
+
bufferedAmount?: number;
|
|
36
38
|
url?: string | URL | null;
|
|
37
39
|
protocol?: string | null;
|
|
38
40
|
}
|
|
@@ -52,6 +54,7 @@ declare class WSContext<T = unknown> {
|
|
|
52
54
|
raw?: T;
|
|
53
55
|
binaryType: BinaryType;
|
|
54
56
|
get readyState(): WSReadyState;
|
|
57
|
+
get bufferedAmount(): number;
|
|
55
58
|
url: URL | null;
|
|
56
59
|
protocol: string | null;
|
|
57
60
|
close(code?: number, reason?: string): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { B as NotFoundHandler, y as ErrorHandler } from "./context-DS3fYyG-.js";
|
|
2
2
|
import { t as ConnectionInfo } from "./connection-ZLEso7XL.js";
|
|
3
|
+
import { n as UpgradeWebSocket } from "./index-C-SklY6u.js";
|
|
3
4
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
4
5
|
import z from "@deepseek-ai/schemastery";
|
|
5
6
|
|
|
@@ -18,8 +19,21 @@ declare const Config: z<Partial<Config>, Config>;
|
|
|
18
19
|
* served over HTTP without depending on the adapter package.
|
|
19
20
|
*/
|
|
20
21
|
interface RouterHttpService {
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Serve `prefix` and everything below it through the router; returns a remover. The mount also goes away when the calling plugin unloads.
|
|
24
|
+
* `options.upgrade` lists the exact paths below the prefix that may be upgraded to a WebSocket (each must be inside the prefix); no other path of the mount accepts an upgrade.
|
|
25
|
+
*/
|
|
26
|
+
mount(prefix: string, options?: {
|
|
27
|
+
readonly upgrade?: readonly string[];
|
|
28
|
+
}): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* `upgradeWebSocket` for the calling plugin: `app.get('/terminal', upgrade((c) => ({ onMessage(event, ws) { ... } })))` on a route the plugin mounted with `upgrade: ['/terminal']`.
|
|
31
|
+
* Checks go in ordinary middleware on the route; a handler that returns a `Response` refuses with that status before any handshake. The sockets it accepts belong to the calling
|
|
32
|
+
* plugin: when it unloads they are closed with 1001 and the handler sees `close` once. Needs the `ws` package.
|
|
33
|
+
*/
|
|
34
|
+
upgrades(): UpgradeWebSocket;
|
|
35
|
+
/** How many WebSocket connections are open now. */
|
|
36
|
+
readonly sockets: number;
|
|
23
37
|
}
|
|
24
38
|
declare module '@deepseek-ai/cordis' {
|
|
25
39
|
interface Context {
|
|
@@ -83,8 +97,9 @@ declare class Router extends Service {
|
|
|
83
97
|
* Run a request in process. Never throws: failures become a 500, a disposed router answers 503. `connection` describes the transport
|
|
84
98
|
* (an HTTP adapter passes the socket's peer address and local port); an in-process caller omits it. Handlers read it with
|
|
85
99
|
* `connectionOf(c)`, abort with `c.req.raw.signal` and keep the request in flight with `c.executionCtx.waitUntil(task)`.
|
|
100
|
+
* `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.
|
|
86
101
|
*/
|
|
87
|
-
fetch(request: Request, connection?: ConnectionInfo): Promise<Response>;
|
|
102
|
+
fetch(request: Request, connection?: ConnectionInfo, bindings?: object): Promise<Response>;
|
|
88
103
|
private dispatch;
|
|
89
104
|
private app;
|
|
90
105
|
/**
|
package/lib/node-server.d.ts
CHANGED
|
@@ -1,42 +1,12 @@
|
|
|
1
1
|
import { t as Acryl } from "./app-D8riatvv.js";
|
|
2
|
-
import { n as
|
|
3
|
-
import { t as RequestError } from "./error-t4N2js2I.js";
|
|
2
|
+
import { a as Owner, c as UpgradeHubOptions, d as upgradeWebSocket, f as WebSocketData, i as testWebSocket, l as UpgradeRouter, m as WebSocketServerLike, n as TestWebSocketOptions, o as UpgradeEmitter, p as WebSocketLike, r as TestWebSocketResult, s as UpgradeHub, t as TestSocket, u as RequestError } from "./test-websocket-DyeS50Tr.js";
|
|
4
3
|
import { ServeStaticOptions, serveStatic } from "./node-server/serve-static.js";
|
|
5
4
|
import { getConnInfo } from "./node-server/conninfo.js";
|
|
6
5
|
import { AddressInfo } from "node:net";
|
|
7
6
|
import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http";
|
|
8
7
|
import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2";
|
|
9
|
-
import { Duplex } from "node:stream";
|
|
10
8
|
import { Server as Server$1, ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https";
|
|
11
9
|
|
|
12
|
-
//#region ../../plugins/acryl-node-server/src/adapter/websocket-types.d.ts
|
|
13
|
-
type WSReadyState = 0 | 1 | 2 | 3;
|
|
14
|
-
type WebSocketData = string | ArrayBuffer | Uint8Array | readonly Uint8Array[];
|
|
15
|
-
type WebSocketSendOptions = {
|
|
16
|
-
compress?: boolean;
|
|
17
|
-
};
|
|
18
|
-
interface WebSocketLike {
|
|
19
|
-
protocol: string;
|
|
20
|
-
readyState: WSReadyState;
|
|
21
|
-
close(code?: number, reason?: string): void;
|
|
22
|
-
send(data: string | ArrayBuffer | ArrayBufferView, options?: WebSocketSendOptions): void;
|
|
23
|
-
on(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this;
|
|
24
|
-
on(event: 'close', listener: (code: number, reason: Uint8Array) => void): this;
|
|
25
|
-
on(event: 'error', listener: (error: unknown) => void): this;
|
|
26
|
-
off(event: 'message', listener: (data: WebSocketData, isBinary: boolean) => void): this;
|
|
27
|
-
}
|
|
28
|
-
interface WebSocketServerLike {
|
|
29
|
-
options: {
|
|
30
|
-
noServer?: boolean;
|
|
31
|
-
};
|
|
32
|
-
on(event: 'connection', listener: (ws: WebSocketLike, request: IncomingMessage) => void): this;
|
|
33
|
-
on(event: 'headers', listener: (headers: string[]) => void): this;
|
|
34
|
-
off(event: 'headers', listener: (headers: string[]) => void): this;
|
|
35
|
-
emit(event: 'connection', ws: WebSocketLike, request: IncomingMessage): boolean;
|
|
36
|
-
handleUpgrade(request: IncomingMessage, socket: Duplex, head: Buffer, callback: (ws: WebSocketLike) => void): void;
|
|
37
|
-
close(): void;
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
10
|
//#region ../../plugins/acryl-node-server/src/adapter/types.d.ts
|
|
41
11
|
type HttpBindings = {
|
|
42
12
|
incoming: IncomingMessage;
|
|
@@ -107,12 +77,6 @@ declare function serve(options: HttpsOptions, listeningListener?: (info: Address
|
|
|
107
77
|
declare function serve(options: HttpOptions, listeningListener?: (info: AddressInfo) => void): Server;
|
|
108
78
|
declare function serve(options: Options, listeningListener?: (info: AddressInfo) => void): ServerType;
|
|
109
79
|
//#endregion
|
|
110
|
-
//#region ../../plugins/acryl-node-server/src/adapter/websocket.d.ts
|
|
111
|
-
type UpgradeWebSocketOptions = {
|
|
112
|
-
onError: (err: unknown) => void;
|
|
113
|
-
};
|
|
114
|
-
declare const upgradeWebSocket: UpgradeWebSocket<WebSocketLike, UpgradeWebSocketOptions>;
|
|
115
|
-
//#endregion
|
|
116
80
|
//#region ../../plugins/acryl-node-server/src/adapter/listener.d.ts
|
|
117
81
|
declare const getRequestListener: (fetchCallback: FetchCallback, options?: {
|
|
118
82
|
hostname?: string;
|
|
@@ -161,4 +125,4 @@ declare function clientModules(options: ClientModulesOptions): Acryl;
|
|
|
161
125
|
*/
|
|
162
126
|
declare function buildClientModules(options: BuildClientModulesOptions): string[];
|
|
163
127
|
//#endregion
|
|
164
|
-
export { type BuildClientModulesOptions, type ClientModulesOptions, type Http2Bindings, type HttpBindings, RequestError, type ServeStaticOptions, type ServerType, type WebSocketData, type WebSocketLike, type WebSocketServerLike, buildClientModules, clientModules, createAdaptorServer, getConnInfo, getRequestListener, serve, serveStatic, upgradeWebSocket };
|
|
128
|
+
export { type BuildClientModulesOptions, type ClientModulesOptions, type Http2Bindings, type HttpBindings, type Owner, RequestError, type ServeStaticOptions, type ServerType, type TestSocket, type TestWebSocketOptions, type TestWebSocketResult, UpgradeEmitter, UpgradeHub, type UpgradeHubOptions, type UpgradeRouter, type WebSocketData, type WebSocketLike, type WebSocketServerLike, buildClientModules, clientModules, createAdaptorServer, getConnInfo, getRequestListener, serve, serveStatic, testWebSocket, upgradeWebSocket };
|
package/lib/node-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { a as serve, i as createAdaptorServer, l as RequestError, n as UpgradeEmitter, o as upgradeWebSocket, r as UpgradeHub, s as getRequestListener, t as testWebSocket } from "./test-websocket-ZXHNHQSU.js";
|
|
2
2
|
import { t as serveStatic } from "./serve-static-3uo_9rjF.js";
|
|
3
3
|
import { getConnInfo } from "./node-server/conninfo.js";
|
|
4
4
|
import { n as clientModules, t as buildClientModules } from "./client-modules-CLvWOjbd.js";
|
|
5
|
-
export { RequestError, buildClientModules, clientModules, createAdaptorServer, getConnInfo, getRequestListener, serve, serveStatic, upgradeWebSocket };
|
|
5
|
+
export { RequestError, UpgradeEmitter, UpgradeHub, buildClientModules, clientModules, createAdaptorServer, getConnInfo, getRequestListener, serve, serveStatic, testWebSocket, upgradeWebSocket };
|
|
@@ -5,7 +5,7 @@ import { r as streamSSE } from "./streaming-DHST3Scg.js";
|
|
|
5
5
|
import { loopbackAuthority, loopbackOnly } from "./loopback-only.js";
|
|
6
6
|
import "./helpers-CvTauQT5.js";
|
|
7
7
|
import { UiControlError, WINDOW_ID, parseChannelToHost } from "./door/protocol.js";
|
|
8
|
-
import "./src-
|
|
8
|
+
import "./src-DNzw0Qsb.js";
|
|
9
9
|
import "./middleware-CPQ8himN.js";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { timingSafeEqual } from "node:crypto";
|
|
@@ -186,8 +186,9 @@ var Router = class extends Service {
|
|
|
186
186
|
* Run a request in process. Never throws: failures become a 500, a disposed router answers 503. `connection` describes the transport
|
|
187
187
|
* (an HTTP adapter passes the socket's peer address and local port); an in-process caller omits it. Handlers read it with
|
|
188
188
|
* `connectionOf(c)`, abort with `c.req.raw.signal` and keep the request in flight with `c.executionCtx.waitUntil(task)`.
|
|
189
|
+
* `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.
|
|
189
190
|
*/
|
|
190
|
-
fetch(request, connection = {}) {
|
|
191
|
+
fetch(request, connection = {}, bindings) {
|
|
191
192
|
if (this.state.closing) return Promise.resolve(new Response("Service Unavailable", { status: 503 }));
|
|
192
193
|
const controller = new AbortController();
|
|
193
194
|
const forward = () => controller.abort();
|
|
@@ -201,7 +202,7 @@ var Router = class extends Service {
|
|
|
201
202
|
passThroughOnException: () => {},
|
|
202
203
|
props: {}
|
|
203
204
|
};
|
|
204
|
-
const env = { connection };
|
|
205
|
+
const env = bindings === void 0 ? { connection } : Object.assign(bindings, { connection });
|
|
205
206
|
let bodyDone = Promise.resolve();
|
|
206
207
|
let endBody = () => {};
|
|
207
208
|
let resolveSettled;
|