zastro-websockets-node 9.3.1-1 → 9.5.2-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +11 -0
- package/dist/index.js +14 -3
- package/dist/middleware.d.ts +2 -2
- package/dist/middleware.js +2 -2
- package/dist/serve-app.d.ts +2 -2
- package/dist/serve-app.js +15 -11
- package/dist/serve-static.js +7 -3
- package/dist/server.d.ts +1 -0
- package/dist/server.js +4 -2
- package/dist/standalone.js +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/websocket/attach.d.ts +12 -0
- package/dist/websocket/attach.js +2 -9
- package/dist/websocket/dev-middleware.d.ts +12 -1
- package/dist/websocket/dev-middleware.js +90 -7
- package/dist/websocket/response.d.ts +9 -1
- package/dist/websocket/response.js +6 -22
- package/dist/websocket/serve-websocket.js +8 -4
- package/dist/websocket/stats.d.ts +1 -0
- package/dist/websocket/stats.js +12 -1
- package/dist/websocket/websocket.d.ts +1 -6
- package/dist/websocket/websocket.js +25 -38
- package/package.json +11 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { AstroAdapter, AstroIntegration } from 'astro';
|
|
2
2
|
import type { Options, UserOptions } from './types.js';
|
|
3
3
|
export declare function getAdapter(options: Options): AstroAdapter;
|
|
4
|
+
declare global {
|
|
5
|
+
namespace App {
|
|
6
|
+
interface Locals {
|
|
7
|
+
isUpgradeRequest?: boolean;
|
|
8
|
+
upgradeWebSocket?: () => {
|
|
9
|
+
socket: import('./websocket/websocket.js').WebSocket;
|
|
10
|
+
response: import('./websocket/response.js').UpgradeResponse;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
export default function createIntegration(userOptions: UserOptions): AstroIntegration;
|
package/dist/index.js
CHANGED
|
@@ -24,17 +24,24 @@ function getAdapter(options) {
|
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
const protocols = ["http:", "https:"];
|
|
27
28
|
function createIntegration(userOptions) {
|
|
28
29
|
if (!userOptions?.mode) {
|
|
29
30
|
throw new AstroError(`Setting the 'mode' option is required.`);
|
|
30
31
|
}
|
|
32
|
+
const { experimentalErrorPageHost } = userOptions;
|
|
33
|
+
if (experimentalErrorPageHost && (!URL.canParse(experimentalErrorPageHost) || !protocols.includes(new URL(experimentalErrorPageHost).protocol))) {
|
|
34
|
+
throw new AstroError(
|
|
35
|
+
`Invalid experimentalErrorPageHost: ${experimentalErrorPageHost}. It should be a valid URL.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
31
38
|
let _options;
|
|
32
39
|
let _config = void 0;
|
|
33
40
|
let _routeToHeaders = void 0;
|
|
34
41
|
return {
|
|
35
42
|
name: "zastro-websockets-node",
|
|
36
43
|
hooks: {
|
|
37
|
-
"astro:config:setup": async ({ updateConfig, config, logger }) => {
|
|
44
|
+
"astro:config:setup": async ({ updateConfig, config, logger, command }) => {
|
|
38
45
|
let session = config.session;
|
|
39
46
|
_config = config;
|
|
40
47
|
if (!session?.driver) {
|
|
@@ -48,10 +55,13 @@ function createIntegration(userOptions) {
|
|
|
48
55
|
};
|
|
49
56
|
}
|
|
50
57
|
updateConfig({
|
|
58
|
+
build: {
|
|
59
|
+
redirects: false
|
|
60
|
+
},
|
|
51
61
|
image: {
|
|
52
62
|
endpoint: {
|
|
53
63
|
route: config.image.endpoint.route ?? "_image",
|
|
54
|
-
entrypoint: config.image.endpoint.entrypoint ?? "astro/assets/endpoint/node"
|
|
64
|
+
entrypoint: config.image.endpoint.entrypoint ?? (command === "dev" ? "astro/assets/endpoint/dev" : "astro/assets/endpoint/node")
|
|
55
65
|
}
|
|
56
66
|
},
|
|
57
67
|
session,
|
|
@@ -73,7 +83,8 @@ function createIntegration(userOptions) {
|
|
|
73
83
|
host: config.server.host,
|
|
74
84
|
port: config.server.port,
|
|
75
85
|
assets: config.build.assets,
|
|
76
|
-
experimentalStaticHeaders: userOptions.experimentalStaticHeaders ?? false
|
|
86
|
+
experimentalStaticHeaders: userOptions.experimentalStaticHeaders ?? false,
|
|
87
|
+
experimentalErrorPageHost
|
|
77
88
|
};
|
|
78
89
|
setAdapter(getAdapter(_options));
|
|
79
90
|
},
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NodeApp } from 'astro/app/node';
|
|
2
|
-
import type { RequestHandler } from './types.js';
|
|
2
|
+
import type { Options, RequestHandler } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a middleware that can be used with Express, Connect, etc.
|
|
5
5
|
*
|
|
@@ -8,4 +8,4 @@ import type { RequestHandler } from './types.js';
|
|
|
8
8
|
*
|
|
9
9
|
* https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling
|
|
10
10
|
*/
|
|
11
|
-
export default function createMiddleware(app: NodeApp): RequestHandler;
|
|
11
|
+
export default function createMiddleware(app: NodeApp, options: Options): RequestHandler;
|
package/dist/middleware.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createAppHandler } from "./serve-app.js";
|
|
2
|
-
function createMiddleware(app) {
|
|
3
|
-
const handler = createAppHandler(app);
|
|
2
|
+
function createMiddleware(app, options) {
|
|
3
|
+
const handler = createAppHandler(app, options);
|
|
4
4
|
const logger = app.getAdapterLogger();
|
|
5
5
|
return async (...args) => {
|
|
6
6
|
const [req, res, next, locals] = args;
|
package/dist/serve-app.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { NodeApp } from 'astro/app/node';
|
|
2
|
-
import type { RequestHandler } from './types.js';
|
|
2
|
+
import type { Options, RequestHandler } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a Node.js http listener for on-demand rendered pages, compatible with http.createServer and Connect middleware.
|
|
5
5
|
* If the next callback is provided, it will be called if the request does not have a matching route.
|
|
6
6
|
* Intended to be used in both standalone and middleware mode.
|
|
7
7
|
*/
|
|
8
|
-
export declare function createAppHandler(app: NodeApp): RequestHandler;
|
|
8
|
+
export declare function createAppHandler(app: NodeApp, options: Options): RequestHandler;
|
package/dist/serve-app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { NodeApp } from "astro/app/node";
|
|
3
|
-
function createAppHandler(app) {
|
|
3
|
+
function createAppHandler(app, options) {
|
|
4
4
|
const als = new AsyncLocalStorage();
|
|
5
5
|
const logger = app.getAdapterLogger();
|
|
6
6
|
process.on("unhandledRejection", (reason) => {
|
|
@@ -8,10 +8,19 @@ function createAppHandler(app) {
|
|
|
8
8
|
logger.error(`Unhandled rejection while rendering ${requestUrl}`);
|
|
9
9
|
console.error(reason);
|
|
10
10
|
});
|
|
11
|
+
const originUrl = options.experimentalErrorPageHost ? new URL(options.experimentalErrorPageHost) : void 0;
|
|
12
|
+
const prerenderedErrorPageFetch = originUrl ? (url) => {
|
|
13
|
+
const errorPageUrl = new URL(url);
|
|
14
|
+
errorPageUrl.protocol = originUrl.protocol;
|
|
15
|
+
errorPageUrl.host = originUrl.host;
|
|
16
|
+
return fetch(errorPageUrl);
|
|
17
|
+
} : void 0;
|
|
11
18
|
return async (req, res, next, locals = {}) => {
|
|
12
19
|
let request;
|
|
13
20
|
try {
|
|
14
|
-
request = NodeApp.createRequest(req
|
|
21
|
+
request = NodeApp.createRequest(req, {
|
|
22
|
+
allowedDomains: app.getAllowedDomains?.() ?? []
|
|
23
|
+
});
|
|
15
24
|
} catch (err) {
|
|
16
25
|
logger.error(`Could not render ${req.url}`);
|
|
17
26
|
console.error(err);
|
|
@@ -19,27 +28,22 @@ function createAppHandler(app) {
|
|
|
19
28
|
res.end("Internal Server Error");
|
|
20
29
|
return;
|
|
21
30
|
}
|
|
22
|
-
|
|
23
|
-
isUpgradeRequest: false,
|
|
24
|
-
upgradeWebSocket() {
|
|
25
|
-
throw new Error("The request must be an upgrade request to upgrade the connection to a WebSocket.");
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
const routeData = app.match(request);
|
|
31
|
+
const routeData = app.match(request, true);
|
|
29
32
|
if (routeData) {
|
|
30
33
|
const response = await als.run(
|
|
31
34
|
request.url,
|
|
32
35
|
() => app.render(request, {
|
|
33
36
|
addCookieHeader: true,
|
|
34
37
|
locals,
|
|
35
|
-
routeData
|
|
38
|
+
routeData,
|
|
39
|
+
prerenderedErrorPageFetch
|
|
36
40
|
})
|
|
37
41
|
);
|
|
38
42
|
await NodeApp.writeResponse(response, res);
|
|
39
43
|
} else if (next) {
|
|
40
44
|
return next();
|
|
41
45
|
} else {
|
|
42
|
-
const response = await app.render(req, { addCookieHeader: true });
|
|
46
|
+
const response = await app.render(req, { addCookieHeader: true, prerenderedErrorPageFetch });
|
|
43
47
|
await NodeApp.writeResponse(response, res);
|
|
44
48
|
}
|
|
45
49
|
};
|
package/dist/serve-static.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import url from "node:url";
|
|
4
|
-
import { hasFileExtension } from "@astrojs/internal-helpers/path";
|
|
4
|
+
import { hasFileExtension, isInternalPath } from "@astrojs/internal-helpers/path";
|
|
5
5
|
import send from "send";
|
|
6
6
|
function createStaticHandler(app, options) {
|
|
7
7
|
const client = resolveClientDir(options);
|
|
8
8
|
return (req, res, ssr) => {
|
|
9
9
|
if (req.url) {
|
|
10
|
-
|
|
10
|
+
let fullUrl = req.url;
|
|
11
|
+
if (req.url.includes("#")) {
|
|
12
|
+
fullUrl = fullUrl.slice(0, req.url.indexOf("#"));
|
|
13
|
+
}
|
|
14
|
+
const [urlPath, urlQuery] = fullUrl.split("?");
|
|
11
15
|
const filePath = path.join(client, app.removeBase(urlPath));
|
|
12
16
|
let isDirectory = false;
|
|
13
17
|
try {
|
|
@@ -48,7 +52,7 @@ function createStaticHandler(app, options) {
|
|
|
48
52
|
break;
|
|
49
53
|
}
|
|
50
54
|
case "always": {
|
|
51
|
-
if (!hasSlash && !hasFileExtension(urlPath)) {
|
|
55
|
+
if (!hasSlash && !hasFileExtension(urlPath) && !isInternalPath(urlPath)) {
|
|
52
56
|
pathname = urlPath + "/" + (urlQuery ? "?" + urlQuery : "");
|
|
53
57
|
res.statusCode = 301;
|
|
54
58
|
res.setHeader("Location", pathname);
|
package/dist/server.d.ts
CHANGED
|
@@ -14,5 +14,6 @@ export declare function createExports(manifest: SSRManifest, options: Options):
|
|
|
14
14
|
};
|
|
15
15
|
done: Promise<void>;
|
|
16
16
|
};
|
|
17
|
+
websocketHandler: (req: import("http").IncomingMessage, socket: import("stream").Duplex, head: NonSharedBuffer) => void;
|
|
17
18
|
};
|
|
18
19
|
export declare function start(manifest: SSRManifest, options: Options): void;
|
package/dist/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { setGetEnv } from "astro/env/setup";
|
|
|
5
5
|
import createMiddleware from "./middleware.js";
|
|
6
6
|
import { STATIC_HEADERS_FILE } from "./shared.js";
|
|
7
7
|
import startServer, { createStandaloneHandler } from "./standalone.js";
|
|
8
|
+
import { createWebsocketHandler } from "./websocket/serve-websocket.js";
|
|
8
9
|
setGetEnv((key) => process.env[key]);
|
|
9
10
|
function createExports(manifest, options) {
|
|
10
11
|
const app = new NodeApp(manifest, !options.experimentalDisableStreaming);
|
|
@@ -18,8 +19,9 @@ function createExports(manifest, options) {
|
|
|
18
19
|
options.trailingSlash = manifest.trailingSlash;
|
|
19
20
|
return {
|
|
20
21
|
options,
|
|
21
|
-
handler: options.mode === "middleware" ? createMiddleware(app) : createStandaloneHandler(app, options),
|
|
22
|
-
startServer: () => startServer(app, options)
|
|
22
|
+
handler: options.mode === "middleware" ? createMiddleware(app, options) : createStandaloneHandler(app, options),
|
|
23
|
+
startServer: () => startServer(app, options),
|
|
24
|
+
websocketHandler: createWebsocketHandler(app)
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
function start(manifest, options) {
|
package/dist/standalone.js
CHANGED
|
@@ -28,7 +28,7 @@ function standalone(app, options) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
function createStandaloneHandler(app, options) {
|
|
31
|
-
const appHandler = createAppHandler(app);
|
|
31
|
+
const appHandler = createAppHandler(app, options);
|
|
32
32
|
const staticHandler = createStaticHandler(app, options);
|
|
33
33
|
return (req, res) => {
|
|
34
34
|
try {
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export interface UserOptions {
|
|
|
19
19
|
* - The CSP header of the static pages is added when CSP support is enabled.
|
|
20
20
|
*/
|
|
21
21
|
experimentalStaticHeaders?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The host that should be used if the server needs to fetch the prerendered error page.
|
|
24
|
+
* If not provided, this will default to the host of the server. This should be set if the server
|
|
25
|
+
* should fetch prerendered error pages from a different host than the public URL of the server.
|
|
26
|
+
* This is useful for example if the server is behind a reverse proxy or a load balancer, or if
|
|
27
|
+
* static files are hosted on a different domain. Do not include a path in the URL: it will be ignored.
|
|
28
|
+
*/
|
|
29
|
+
experimentalErrorPageHost?: string | URL;
|
|
22
30
|
}
|
|
23
31
|
export interface Options extends UserOptions {
|
|
24
32
|
host: string | boolean;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import type * as ws from "ws";
|
|
2
2
|
import type { WebSocket } from "./websocket.js";
|
|
3
|
+
/**
|
|
4
|
+
* To keep the internals hidden, the function that attaches the
|
|
5
|
+
* ws.WebSocket to the WebSocket instance is created within
|
|
6
|
+
* WebSocket's static block, and assigned to this variable.
|
|
7
|
+
*/
|
|
8
|
+
export declare const attacher: {
|
|
9
|
+
attach: null | typeof attach;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Attach a ws.WebSocket connected to I/O to the implementation
|
|
13
|
+
* of the standard WebSocket class exposed to the public API.
|
|
14
|
+
*/
|
|
3
15
|
export declare function attach(standard: WebSocket, ws: ws.WebSocket): void;
|
package/dist/websocket/attach.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
const wsMap = /* @__PURE__ */ new WeakMap();
|
|
2
1
|
const attacher = { attach: null };
|
|
3
|
-
function attachImpl(standard, ws) {
|
|
4
|
-
if (wsMap.has(standard)) {
|
|
5
|
-
throw new Error("WebSocket already attached");
|
|
6
|
-
}
|
|
7
|
-
wsMap.set(standard, ws);
|
|
8
|
-
}
|
|
9
|
-
attacher.attach = attachImpl;
|
|
10
2
|
function attach(standard, ws) {
|
|
11
3
|
return attacher.attach?.(standard, ws);
|
|
12
4
|
}
|
|
13
5
|
export {
|
|
14
|
-
attach
|
|
6
|
+
attach,
|
|
7
|
+
attacher
|
|
15
8
|
};
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import type { APIContext, AstroIntegration, MiddlewareNext } from "astro";
|
|
2
|
+
export type ViteDevServer = Parameters<NonNullable<AstroIntegration["hooks"]["astro:server:setup"]>>[0]["server"];
|
|
3
|
+
/**
|
|
4
|
+
* This dev-only middleware is responsible for all requests
|
|
5
|
+
* that have been made as a result of an upgrade request.
|
|
6
|
+
*
|
|
7
|
+
* It checks whether the request is running within the context
|
|
8
|
+
* of an `upgradeRequestStorage`, which are created below in
|
|
9
|
+
* `hookIntoViteDevServer()`, and only runs if it is.
|
|
10
|
+
*/
|
|
11
|
+
export declare const onRequest: (context: APIContext, next: MiddlewareNext) => Promise<Response>;
|
|
12
|
+
export declare function handleUpgradeRequests(viteDevServer: ViteDevServer): void;
|
|
@@ -1,12 +1,95 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import * as ws from "ws";
|
|
3
|
+
import { UpgradeResponse, writeResponseToSocket } from "./response.js";
|
|
4
|
+
import { WebSocket } from "./websocket.js";
|
|
5
|
+
import { attach as _attach } from "./attach.js";
|
|
6
|
+
const upgradeRequestStorage = (
|
|
7
|
+
// @ts-expect-error
|
|
8
|
+
globalThis.__upgradeRequestStorage ??= new AsyncLocalStorage()
|
|
9
|
+
);
|
|
10
|
+
const responseToSocketMap = (
|
|
11
|
+
// @ts-expect-error
|
|
12
|
+
globalThis.__responseToSocketMap ??= /* @__PURE__ */ new WeakMap()
|
|
13
|
+
);
|
|
14
|
+
globalThis.__UpgradeResponse = UpgradeResponse;
|
|
15
|
+
function newUpgradeResponse() {
|
|
16
|
+
return new globalThis.__UpgradeResponse();
|
|
17
|
+
}
|
|
18
|
+
globalThis.__WebSocket = WebSocket;
|
|
19
|
+
function newWebSocket() {
|
|
20
|
+
return new globalThis.__WebSocket();
|
|
21
|
+
}
|
|
22
|
+
globalThis.__attach = _attach;
|
|
23
|
+
function attach(...args) {
|
|
24
|
+
return globalThis.__attach(...args);
|
|
25
|
+
}
|
|
1
26
|
const onRequest = async function websocketDevMiddleware(context, next) {
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
27
|
+
const upgradeRequest = upgradeRequestStorage.getStore();
|
|
28
|
+
if (upgradeRequest === void 0) {
|
|
29
|
+
if (!context.locals.isUpgradeRequest) {
|
|
30
|
+
Object.assign(context.locals, {
|
|
31
|
+
isUpgradeRequest: false,
|
|
32
|
+
upgradeWebSocket() {
|
|
33
|
+
throw new Error("The request must be an upgrade request to upgrade the connection to a WebSocket.");
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return next();
|
|
38
|
+
}
|
|
39
|
+
let response;
|
|
40
|
+
let error;
|
|
41
|
+
try {
|
|
42
|
+
response = await next();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
error = e;
|
|
45
|
+
}
|
|
46
|
+
if (response) {
|
|
47
|
+
if (response instanceof UpgradeResponse) {
|
|
48
|
+
const standardWebSocket = responseToSocketMap.get(response);
|
|
49
|
+
const [wsServer, req, socket, head] = upgradeRequest;
|
|
50
|
+
wsServer.handleUpgrade(req, socket, head, (ws2) => attach(standardWebSocket, ws2));
|
|
51
|
+
} else {
|
|
52
|
+
const socket = upgradeRequest[2];
|
|
53
|
+
await writeResponseToSocket(socket, response);
|
|
54
|
+
}
|
|
55
|
+
return response;
|
|
56
|
+
}
|
|
57
|
+
await writeResponseToSocket(upgradeRequest[2], new Response(null, { status: 500 }));
|
|
58
|
+
if (error && error instanceof Error) throw error;
|
|
59
|
+
throw new Error("Unknown error", { cause: error });
|
|
60
|
+
};
|
|
61
|
+
const devLocals = {
|
|
62
|
+
isUpgradeRequest: true,
|
|
63
|
+
upgradeWebSocket() {
|
|
64
|
+
const response = newUpgradeResponse();
|
|
65
|
+
const socket = newWebSocket();
|
|
66
|
+
responseToSocketMap.set(response, socket);
|
|
67
|
+
return { socket, response };
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
function handleUpgradeRequests(viteDevServer) {
|
|
71
|
+
const astroDevHandler = viteDevServer.middlewares.stack.find((stackItem) => "name" in stackItem.handle && stackItem.handle.name === "astroDevHandler").handle;
|
|
72
|
+
const wsServer = new ws.WebSocketServer({ noServer: true });
|
|
73
|
+
const httpServer = viteDevServer.httpServer;
|
|
74
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
75
|
+
if (req.headers["sec-websocket-protocol"] === "vite-hmr") return;
|
|
76
|
+
req[Symbol.for("astro.locals")] = devLocals;
|
|
77
|
+
upgradeRequestStorage.run([wsServer, req, socket, head], astroDevHandler, req, fakeResponse);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const fakeResponse = {
|
|
81
|
+
setHeader() {
|
|
82
|
+
},
|
|
83
|
+
write() {
|
|
84
|
+
},
|
|
85
|
+
writeHead() {
|
|
86
|
+
},
|
|
87
|
+
end() {
|
|
88
|
+
},
|
|
89
|
+
on() {
|
|
90
|
+
}
|
|
9
91
|
};
|
|
10
92
|
export {
|
|
93
|
+
handleUpgradeRequests,
|
|
11
94
|
onRequest
|
|
12
95
|
};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom subclass because spec-compliant Response objects can't have a status of 101.
|
|
3
|
+
*/
|
|
1
4
|
export declare class UpgradeResponse extends Response {
|
|
2
5
|
readonly status = 101;
|
|
3
|
-
constructor();
|
|
4
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* The "upgrade" event callback doesn't provide a response object.
|
|
9
|
+
* If the userland code decides that protocol should not be upgraded,
|
|
10
|
+
* the rejection response must be manually streamed into the lower
|
|
11
|
+
* level socket.
|
|
12
|
+
*/
|
|
5
13
|
export declare function writeResponseToSocket(socket: import("node:stream").Duplex, response: Response): Promise<void>;
|
|
@@ -1,32 +1,16 @@
|
|
|
1
1
|
import { pipeline } from "node:stream/promises";
|
|
2
2
|
class UpgradeResponse extends Response {
|
|
3
3
|
status = 101;
|
|
4
|
-
constructor() {
|
|
5
|
-
super(null, {
|
|
6
|
-
status: 200,
|
|
7
|
-
statusText: "Switching Protocols",
|
|
8
|
-
headers: {
|
|
9
|
-
"Upgrade": "websocket",
|
|
10
|
-
"Connection": "Upgrade"
|
|
11
|
-
}
|
|
12
|
-
});
|
|
13
|
-
Object.defineProperty(this, "status", {
|
|
14
|
-
value: 101,
|
|
15
|
-
writable: false,
|
|
16
|
-
enumerable: true,
|
|
17
|
-
configurable: false
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
4
|
}
|
|
5
|
+
const { Headers } = globalThis;
|
|
21
6
|
async function writeResponseToSocket(socket, response) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
head += `\r
|
|
25
|
-
`;
|
|
26
|
-
for (const [name, value] of response.headers) {
|
|
27
|
-
head += `${name}: ${value}\r
|
|
7
|
+
const { headers, status, statusText } = response;
|
|
8
|
+
let head = `HTTP/1.1 ${status} ${statusText}\r
|
|
28
9
|
`;
|
|
10
|
+
for (const [header, value] of new Headers(headers).entries()) {
|
|
11
|
+
head += header + ": " + value + "\r\n";
|
|
29
12
|
}
|
|
13
|
+
socket.on("error", console.error);
|
|
30
14
|
socket.write(head + "\r\n");
|
|
31
15
|
if (response.body) {
|
|
32
16
|
await pipeline(response.clone().body, socket);
|
|
@@ -3,6 +3,7 @@ import { NodeApp } from "astro/app/node";
|
|
|
3
3
|
import { WebSocket } from "./websocket.js";
|
|
4
4
|
import { attach } from "./attach.js";
|
|
5
5
|
import { UpgradeResponse, writeResponseToSocket } from "./response.js";
|
|
6
|
+
import { registerConnection } from "./stats.js";
|
|
6
7
|
function createWebsocketHandler(app) {
|
|
7
8
|
const responseToSocketMap = /* @__PURE__ */ new WeakMap();
|
|
8
9
|
const server = new ws.WebSocketServer({ noServer: true });
|
|
@@ -12,16 +13,19 @@ function createWebsocketHandler(app) {
|
|
|
12
13
|
locals: {
|
|
13
14
|
isUpgradeRequest: true,
|
|
14
15
|
upgradeWebSocket() {
|
|
15
|
-
const
|
|
16
|
+
const websocket = new WebSocket();
|
|
16
17
|
const response2 = new UpgradeResponse();
|
|
17
|
-
responseToSocketMap.set(response2,
|
|
18
|
-
return { socket:
|
|
18
|
+
responseToSocketMap.set(response2, websocket);
|
|
19
|
+
return { socket: websocket, response: response2 };
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
});
|
|
22
23
|
if (response instanceof UpgradeResponse) {
|
|
23
24
|
const websocket = responseToSocketMap.get(response);
|
|
24
|
-
server.handleUpgrade(req, socket, head, (wsSocket) =>
|
|
25
|
+
server.handleUpgrade(req, socket, head, (wsSocket) => {
|
|
26
|
+
registerConnection(websocket, wsSocket);
|
|
27
|
+
attach(websocket, wsSocket);
|
|
28
|
+
});
|
|
25
29
|
} else {
|
|
26
30
|
await writeResponseToSocket(socket, response);
|
|
27
31
|
}
|
package/dist/websocket/stats.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
class WebSocketStatsManager {
|
|
2
2
|
connections = /* @__PURE__ */ new Map();
|
|
3
3
|
connectionCounter = 0;
|
|
4
|
+
lifetimeConnections = 0;
|
|
4
5
|
registerConnection(socket, wsSocket) {
|
|
5
6
|
const id = `ws_${++this.connectionCounter}_${Date.now()}`;
|
|
6
7
|
this.connections.set(id, { socket, wsSocket });
|
|
8
|
+
this.lifetimeConnections++;
|
|
9
|
+
wsSocket.on("close", () => {
|
|
10
|
+
this.connections.delete(id);
|
|
11
|
+
});
|
|
7
12
|
return id;
|
|
8
13
|
}
|
|
9
14
|
getConnectionCount() {
|
|
10
15
|
return this.connections.size;
|
|
11
16
|
}
|
|
17
|
+
getConnectionStats() {
|
|
18
|
+
return {
|
|
19
|
+
totalConnections: this.connections.size,
|
|
20
|
+
totalConnectionsEver: this.lifetimeConnections
|
|
21
|
+
};
|
|
22
|
+
}
|
|
12
23
|
}
|
|
13
24
|
const statsManager = new WebSocketStatsManager();
|
|
14
25
|
const WebSocketStats = {
|
|
15
26
|
getConnectionCount: () => statsManager.getConnectionCount(),
|
|
16
|
-
getConnectionStats: () =>
|
|
27
|
+
getConnectionStats: () => statsManager.getConnectionStats(),
|
|
17
28
|
shutdown: () => {
|
|
18
29
|
}
|
|
19
30
|
};
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type * as ws from "ws";
|
|
2
1
|
type WebSocketInterface = globalThis.WebSocket;
|
|
3
|
-
export declare const attacher: {
|
|
4
|
-
attach: null | typeof attachImpl;
|
|
5
|
-
};
|
|
6
2
|
export declare class WebSocket extends EventTarget implements WebSocketInterface {
|
|
3
|
+
#private;
|
|
7
4
|
static readonly CONNECTING: 0;
|
|
8
5
|
static readonly OPEN: 1;
|
|
9
6
|
static readonly CLOSING: 2;
|
|
@@ -26,7 +23,6 @@ export declare class WebSocket extends EventTarget implements WebSocketInterface
|
|
|
26
23
|
set binaryType(value: "arraybuffer" | "blob");
|
|
27
24
|
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
28
25
|
}
|
|
29
|
-
declare function attachImpl(standard: WebSocket, ws: ws.WebSocket): void;
|
|
30
26
|
export declare class ErrorEvent extends Event {
|
|
31
27
|
readonly error: Error;
|
|
32
28
|
readonly message: string;
|
|
@@ -38,7 +34,6 @@ export declare class CloseEvent extends Event implements globalThis.CloseEvent {
|
|
|
38
34
|
readonly wasClean: boolean;
|
|
39
35
|
constructor(type: string, eventInitDict: CloseEventInit);
|
|
40
36
|
}
|
|
41
|
-
export declare function attach(standard: WebSocket, ws: ws.WebSocket): void;
|
|
42
37
|
interface CloseEventInit extends EventInit {
|
|
43
38
|
code?: number;
|
|
44
39
|
reason?: string;
|
|
@@ -1,57 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
const attacher = { attach: null };
|
|
1
|
+
import { attacher } from "./attach.js";
|
|
3
2
|
class WebSocket extends EventTarget {
|
|
3
|
+
// Use private field like the original patch
|
|
4
|
+
#ws;
|
|
4
5
|
static CONNECTING = 0;
|
|
5
6
|
static OPEN = 1;
|
|
6
7
|
static CLOSING = 2;
|
|
7
8
|
static CLOSED = 3;
|
|
8
9
|
get url() {
|
|
9
|
-
|
|
10
|
-
return ws?.url ?? "";
|
|
10
|
+
return this.#ws?.url ?? "";
|
|
11
11
|
}
|
|
12
12
|
get readyState() {
|
|
13
|
-
|
|
14
|
-
return ws?.readyState ?? this.CONNECTING;
|
|
13
|
+
return this.#ws?.readyState ?? this.CONNECTING;
|
|
15
14
|
}
|
|
16
15
|
get bufferedAmount() {
|
|
17
|
-
|
|
18
|
-
return ws?.bufferedAmount ?? 0;
|
|
16
|
+
return this.#ws?.bufferedAmount ?? 0;
|
|
19
17
|
}
|
|
20
|
-
// networking
|
|
18
|
+
// networking event handlers
|
|
21
19
|
onopen = null;
|
|
22
20
|
onerror = null;
|
|
23
21
|
onclose = null;
|
|
24
22
|
get extensions() {
|
|
25
|
-
|
|
26
|
-
return ws?.extensions ?? "";
|
|
23
|
+
return this.#ws?.extensions ?? "";
|
|
27
24
|
}
|
|
28
25
|
get protocol() {
|
|
29
|
-
|
|
30
|
-
return ws?.protocol ?? "";
|
|
26
|
+
return this.#ws?.protocol ?? "";
|
|
31
27
|
}
|
|
32
28
|
close() {
|
|
33
|
-
|
|
34
|
-
if (ws) ws.close();
|
|
29
|
+
if (this.#ws) this.#ws.close();
|
|
35
30
|
else this.addEventListener("open", () => this.close(), { once: true });
|
|
36
31
|
}
|
|
37
32
|
// messaging
|
|
38
33
|
onmessage = null;
|
|
39
34
|
get binaryType() {
|
|
40
|
-
|
|
41
|
-
return ws?.binaryType ?? "blob";
|
|
35
|
+
return this.#ws?.binaryType ?? "blob";
|
|
42
36
|
}
|
|
43
37
|
set binaryType(value) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Object.assign(ws, { binaryType: value });
|
|
38
|
+
if (this.#ws) {
|
|
39
|
+
this.#ws.binaryType = value;
|
|
47
40
|
} else {
|
|
48
41
|
this.addEventListener("open", () => this.binaryType = value, { once: true });
|
|
49
42
|
}
|
|
50
43
|
}
|
|
51
44
|
send(data) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
else ws.send(data);
|
|
45
|
+
if (data instanceof Blob) data.arrayBuffer().then((buffer) => this.#ws.send(buffer));
|
|
46
|
+
else this.#ws.send(data);
|
|
55
47
|
}
|
|
56
48
|
static {
|
|
57
49
|
Object.assign(this.prototype, {
|
|
@@ -62,18 +54,18 @@ class WebSocket extends EventTarget {
|
|
|
62
54
|
});
|
|
63
55
|
Object.freeze(this.prototype);
|
|
64
56
|
Object.freeze(this);
|
|
65
|
-
attacher.attach =
|
|
57
|
+
attacher.attach = (standard, ws) => {
|
|
58
|
+
if (standard.#ws) {
|
|
59
|
+
throw new Error("WebSocket already attached");
|
|
60
|
+
}
|
|
61
|
+
standard.#ws = ws;
|
|
62
|
+
init(standard, ws);
|
|
63
|
+
return standard;
|
|
64
|
+
};
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
|
-
function attachImpl(standard, ws) {
|
|
69
|
-
if (wsMap.has(standard)) {
|
|
70
|
-
throw new Error("WebSocket already attached");
|
|
71
|
-
}
|
|
72
|
-
wsMap.set(standard, ws);
|
|
73
|
-
init(standard, ws);
|
|
74
|
-
}
|
|
75
67
|
function init(standard, ws) {
|
|
76
|
-
|
|
68
|
+
ws.binaryType = "blob";
|
|
77
69
|
if (ws.readyState === ws.OPEN) {
|
|
78
70
|
const event = new Event("open");
|
|
79
71
|
standard.onopen?.(event);
|
|
@@ -118,13 +110,8 @@ class CloseEvent extends Event {
|
|
|
118
110
|
this.wasClean = eventInitDict.wasClean ?? false;
|
|
119
111
|
}
|
|
120
112
|
}
|
|
121
|
-
function attach(standard, ws) {
|
|
122
|
-
return attacher.attach?.(standard, ws);
|
|
123
|
-
}
|
|
124
113
|
export {
|
|
125
114
|
CloseEvent,
|
|
126
115
|
ErrorEvent,
|
|
127
|
-
WebSocket
|
|
128
|
-
attach,
|
|
129
|
-
attacher
|
|
116
|
+
WebSocket
|
|
130
117
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zastro-websockets-node",
|
|
3
3
|
"description": "Deploy your site to a Node.js server with WebSocket support",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.5.2-2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"author": "Zach Handley <zach@zachhandley.com>",
|
|
@@ -20,19 +20,25 @@
|
|
|
20
20
|
"./server.js": "./dist/server.js",
|
|
21
21
|
"./preview.js": "./dist/preview.js",
|
|
22
22
|
"./package.json": "./package.json",
|
|
23
|
+
"./stats": "./dist/websocket/stats.js",
|
|
23
24
|
"./websocket": "./dist/websocket/index.js",
|
|
24
|
-
"./
|
|
25
|
+
"./websocket/dev-middleware.js": "./dist/websocket/dev-middleware.js",
|
|
26
|
+
"./websocket/websocket.js": "./dist/websocket/websocket.js",
|
|
27
|
+
"./websocket/response.js": "./dist/websocket/response.js",
|
|
28
|
+
"./websocket/serve-websocket.js": "./dist/websocket/serve-websocket.js",
|
|
29
|
+
"./websocket/attach.js": "./dist/websocket/attach.js",
|
|
30
|
+
"./websocket/stats.js": "./dist/websocket/stats.js"
|
|
25
31
|
},
|
|
26
32
|
"files": [
|
|
27
33
|
"dist"
|
|
28
34
|
],
|
|
29
35
|
"dependencies": {
|
|
30
|
-
"@astrojs/internal-helpers": "^0.
|
|
31
|
-
"send": "^1.2.
|
|
36
|
+
"@astrojs/internal-helpers": "^0.7.5",
|
|
37
|
+
"send": "^1.2.1",
|
|
32
38
|
"server-destroy": "^1.0.1",
|
|
33
39
|
"ws": "^8.18.0"
|
|
34
40
|
},
|
|
35
41
|
"peerDependencies": {
|
|
36
|
-
"astro": "^5.3
|
|
42
|
+
"astro": "^5.14.3"
|
|
37
43
|
}
|
|
38
44
|
}
|