mirror of
https://github.com/LukeHagar/crossws.git
synced 2025-12-10 12:27:46 +00:00
feat: dynamic resolver
This commit is contained in:
@@ -66,12 +66,13 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "unbuild",
|
||||
"dev": "pnpm play:node",
|
||||
"lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src",
|
||||
"lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src -w",
|
||||
"prepack": "pnpm run build",
|
||||
"play:bun": "bun playground/bun.ts",
|
||||
"play:cf": "wrangler dev --port 3001",
|
||||
"play:deno": "deno run -A playground/deno.ts",
|
||||
"play:deno": "deno run --unstable-sloppy-imports -A playground/deno.ts",
|
||||
"play:node": "jiti playground/node.ts",
|
||||
"play:uws": "jiti playground/uws.ts",
|
||||
"release": "pnpm test && changelogen --release && npm publish && git push --follow-tags",
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import type { WebSocketHooks, WebSocketAdapter } from "../src";
|
||||
|
||||
export const getIndexHTML = (params) =>
|
||||
import("../examples/h3/index.html.ts").then((r) => r.html(params));
|
||||
|
||||
export function createDemo<T extends WebSocketAdapter>(
|
||||
adapter: T,
|
||||
opts?: Parameters<T>[1],
|
||||
): ReturnType<T> {
|
||||
const hooks = createWebSocketDebugHooks({
|
||||
open(peer) {
|
||||
peer.send(`Hello!`);
|
||||
peer.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
url: peer.url,
|
||||
headers: peer.headers && Object.fromEntries(peer.headers),
|
||||
},
|
||||
undefined,
|
||||
2,
|
||||
),
|
||||
);
|
||||
},
|
||||
message(peer, message) {
|
||||
if (message.text() === "ping") {
|
||||
peer.send("pong");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return adapter(hooks, opts);
|
||||
}
|
||||
|
||||
function createWebSocketDebugHooks(
|
||||
hooks: Partial<WebSocketHooks>,
|
||||
): Partial<WebSocketHooks> {
|
||||
const createDebugHook =
|
||||
(name: keyof WebSocketHooks) =>
|
||||
(peer, ...args: any[]) => {
|
||||
console.log(
|
||||
`[ws] [${name}]`,
|
||||
peer,
|
||||
[...args].map((arg, i) => `\n - arg#${i} ${arg}`).join(""),
|
||||
);
|
||||
hooks[name]?.(peer, ...args);
|
||||
};
|
||||
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get(_, prop) {
|
||||
return createDebugHook(prop as keyof WebSocketHooks);
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
59
playground/_shared.ts
Normal file
59
playground/_shared.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
CrossWSOptions,
|
||||
WebSocketAdapter,
|
||||
defineWebSocketHooks,
|
||||
} from "../src/index.ts";
|
||||
|
||||
export const getIndexHTML = (params) =>
|
||||
import("../examples/h3/index.html.ts").then((r) => r.html(params));
|
||||
|
||||
export function createDemo<T extends WebSocketAdapter>(
|
||||
adapter: T,
|
||||
opts?: Parameters<T>[1],
|
||||
): ReturnType<T> {
|
||||
const hooks = defineWebSocketHooks({
|
||||
$(name, peer, ...args) {
|
||||
console.log(
|
||||
`$ ${peer} ${name} (${args.map((arg) => stringify(arg)).join(", ")})`,
|
||||
);
|
||||
},
|
||||
open(peer) {
|
||||
peer.send(`Hello ${peer}`);
|
||||
},
|
||||
message(peer, message) {
|
||||
if (message.text() === "ping") {
|
||||
peer.send("pong");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const resolve: CrossWSOptions["resolve"] = (peer) => {
|
||||
return {
|
||||
open: () => {
|
||||
peer.send(
|
||||
JSON.stringify(
|
||||
{
|
||||
url: peer.url,
|
||||
headers: peer.headers && Object.fromEntries(peer.headers),
|
||||
},
|
||||
undefined,
|
||||
2,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return adapter(hooks, {
|
||||
resolve,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
function stringify(val) {
|
||||
const str = val.toString();
|
||||
if (str === "[object Object]") {
|
||||
return val.constructor?.name || "??";
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// You can run this demo using `bun --bun ./bun.ts` or `npm run play:bun` in repo
|
||||
|
||||
import bunAdapter from "../src/adapters/bun";
|
||||
import { createDemo, getIndexHTML } from "./_common";
|
||||
import { createDemo, getIndexHTML } from "./_shared";
|
||||
|
||||
const adapter = createDemo(bunAdapter);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// You can run this demo using `npm run play:cf` in repo
|
||||
import type { Request, ExecutionContext } from "@cloudflare/workers-types";
|
||||
import cloudflareAdapter from "../src/adapters/cloudflare";
|
||||
import { createDemo, getIndexHTML } from "./_common.ts";
|
||||
import { createDemo, getIndexHTML } from "./_shared.ts";
|
||||
|
||||
const { handleUpgrade } = createDemo(cloudflareAdapter);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// You can run this demo using `deno run -A ./deno.ts` or `npm run play:deno` in repo
|
||||
|
||||
import denoAdapter from "../dist/adapters/deno.mjs";
|
||||
import denoAdapter from "../src/adapters/deno.ts";
|
||||
|
||||
// @ts-ignore
|
||||
import type * as _Deno from "../types/lib.deno.d.ts";
|
||||
|
||||
import { createDemo, getIndexHTML } from "./_common.ts";
|
||||
import { createDemo, getIndexHTML } from "./_shared.ts";
|
||||
|
||||
declare global {
|
||||
const Deno: typeof import("@deno/types").Deno;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import nodeAdapter from "../src/adapters/node";
|
||||
import { createDemo, getIndexHTML } from "./_common";
|
||||
import { createDemo, getIndexHTML } from "./_shared";
|
||||
|
||||
const adapter = createDemo(nodeAdapter);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// You can run this demo using `npm run play:node-uws` in repo
|
||||
|
||||
import { App } from "uWebSockets.js";
|
||||
import uwsAdapter from "../src/adapters/uws.ts";
|
||||
import { createDemo, getIndexHTML } from "./_common.ts";
|
||||
import uwsAdapter from "../src/adapters/uws";
|
||||
import { createDemo, getIndexHTML } from "./_shared.ts";
|
||||
|
||||
const adapter = createDemo(uwsAdapter);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { WebSocketHandler, ServerWebSocket, Server } from "bun";
|
||||
|
||||
import { WebSocketMessage } from "../message";
|
||||
import { WebSocketError } from "../error";
|
||||
import { WebSocketPeerBase } from "../peer";
|
||||
import { WebSocketPeer } from "../peer";
|
||||
import { defineWebSocketAdapter } from "../adapter";
|
||||
import { CrossWSOptions, createCrossWS } from "../crossws";
|
||||
|
||||
@@ -28,7 +28,7 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
if (ws.data?._peer) {
|
||||
return ws.data._peer;
|
||||
}
|
||||
const peer = new WebSocketPeer({ bun: { ws } });
|
||||
const peer = new BunPeer({ bun: { ws } });
|
||||
ws.data = ws.data || {};
|
||||
ws.data._peer = peer;
|
||||
return peer;
|
||||
@@ -79,7 +79,7 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
},
|
||||
);
|
||||
|
||||
class WebSocketPeer extends WebSocketPeerBase<{
|
||||
class BunPeer extends WebSocketPeer<{
|
||||
bun: { ws: ServerWebSocket<ContextData> };
|
||||
}> {
|
||||
get id() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type * as _cf from "@cloudflare/workers-types";
|
||||
|
||||
import { WebSocketPeerBase } from "../peer";
|
||||
import { WebSocketPeer } from "../peer";
|
||||
import { defineWebSocketAdapter } from "../adapter.js";
|
||||
import { WebSocketMessage } from "../message";
|
||||
import { WebSocketError } from "../error";
|
||||
@@ -36,14 +36,14 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
const client = pair[0];
|
||||
const server = pair[1];
|
||||
|
||||
const peer = new CloudflareWebSocketPeer({
|
||||
const peer = new CloudflarePeer({
|
||||
cloudflare: { client, server, req, env, context },
|
||||
});
|
||||
|
||||
server.accept();
|
||||
|
||||
crossws.$("cloudflare:accept", peer);
|
||||
hooks.open?.(peer);
|
||||
crossws.open(peer);
|
||||
|
||||
server.addEventListener("message", (event) => {
|
||||
crossws.$("cloudflare:message", peer, event);
|
||||
@@ -73,7 +73,7 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
},
|
||||
);
|
||||
|
||||
class CloudflareWebSocketPeer extends WebSocketPeerBase<{
|
||||
class CloudflarePeer extends WebSocketPeer<{
|
||||
cloudflare: {
|
||||
client: _cf.WebSocket;
|
||||
server: _cf.WebSocket;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { WebSocketMessage } from "../message";
|
||||
import { WebSocketError } from "../error";
|
||||
import { WebSocketPeerBase } from "../peer";
|
||||
import { WebSocketPeer } from "../peer";
|
||||
import { defineWebSocketAdapter } from "../adapter.js";
|
||||
import { CrossWSOptions, createCrossWS } from "../crossws";
|
||||
|
||||
@@ -24,12 +24,12 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
|
||||
const handleUpgrade = (req: Request) => {
|
||||
const upgrade = Deno.upgradeWebSocket(req);
|
||||
const peer = new DenoWebSocketPeer({
|
||||
const peer = new DenoPeer({
|
||||
deno: { ws: upgrade.socket, req },
|
||||
});
|
||||
upgrade.socket.addEventListener("open", () => {
|
||||
crossws.$("deno:open", peer);
|
||||
hooks.open?.(peer);
|
||||
crossws.open(peer);
|
||||
});
|
||||
upgrade.socket.addEventListener("message", (event) => {
|
||||
crossws.$("deno:message", peer, event);
|
||||
@@ -52,7 +52,7 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
},
|
||||
);
|
||||
|
||||
class DenoWebSocketPeer extends WebSocketPeerBase<{
|
||||
class DenoPeer extends WebSocketPeer<{
|
||||
deno: { ws: any; req: Request };
|
||||
}> {
|
||||
get id() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
WebSocketServer,
|
||||
WebSocket as WebSocketT,
|
||||
} from "../../types/ws";
|
||||
import { WebSocketPeerBase } from "../peer";
|
||||
import { WebSocketPeer } from "../peer";
|
||||
import { WebSocketMessage } from "../message";
|
||||
import { WebSocketError } from "../error";
|
||||
import { defineWebSocketAdapter } from "../adapter";
|
||||
@@ -37,22 +37,23 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
}) as WebSocketServer);
|
||||
|
||||
// Unmanaged server-level events
|
||||
wss.on("error", (error) => {
|
||||
crossws.$("node:server-error", error);
|
||||
});
|
||||
wss.on("headers", (headers, request) => {
|
||||
crossws.$("node:server-headers", headers, request);
|
||||
});
|
||||
wss.on("listening", () => {
|
||||
crossws.$("node:server-listening");
|
||||
});
|
||||
wss.on("close", () => {
|
||||
crossws.$("node:server-close");
|
||||
});
|
||||
// TODO: Expose with new API
|
||||
// wss.on("error", (error) => {
|
||||
// crossws.$("node:server-error", error);
|
||||
// });
|
||||
// wss.on("headers", (headers, request) => {
|
||||
// crossws.$("node:server-headers", headers, request);
|
||||
// });
|
||||
// wss.on("listening", () => {
|
||||
// crossws.$("node:server-listening");
|
||||
// });
|
||||
// wss.on("close", () => {
|
||||
// crossws.$("node:server-close");
|
||||
// });
|
||||
|
||||
wss.on("connection", (ws, req) => {
|
||||
const peer = new NodeWebSocketPeer({ node: { ws, req, server: wss } });
|
||||
hooks.open?.(peer);
|
||||
const peer = new NodePeer({ node: { ws, req, server: wss } });
|
||||
crossws.open(peer);
|
||||
|
||||
// Managed socket-level events
|
||||
ws.on("message", (data: RawData, isBinary: boolean) => {
|
||||
@@ -105,7 +106,7 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
},
|
||||
);
|
||||
|
||||
class NodeWebSocketPeer extends WebSocketPeerBase<{
|
||||
class NodePeer extends WebSocketPeer<{
|
||||
node: {
|
||||
server: WebSocketServer;
|
||||
req: IncomingMessage;
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
} from "uWebSockets.js";
|
||||
import { WebSocketPeerBase } from "../peer";
|
||||
import { WebSocketPeer } from "../peer";
|
||||
import { WebSocketMessage } from "../message";
|
||||
import { defineWebSocketAdapter } from "../adapter";
|
||||
import { CrossWSOptions, createCrossWS } from "../crossws";
|
||||
@@ -48,7 +48,7 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
if (userData._peer) {
|
||||
return userData._peer as WebSocketPeer;
|
||||
}
|
||||
const peer = new WebSocketPeer({ uws: { ws, userData } });
|
||||
const peer = new UWSPeer({ uws: { ws, userData } });
|
||||
userData._peer = peer;
|
||||
return peer;
|
||||
};
|
||||
@@ -73,7 +73,7 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
open(ws) {
|
||||
const peer = getPeer(ws);
|
||||
crossws.$("uws:open", peer, ws);
|
||||
hooks.open?.(peer);
|
||||
crossws.open(peer);
|
||||
},
|
||||
ping(ws, message) {
|
||||
const peer = getPeer(ws);
|
||||
@@ -110,18 +110,21 @@ export default defineWebSocketAdapter<Adapter, AdapterOptions>(
|
||||
},
|
||||
);
|
||||
|
||||
class WebSocketPeer extends WebSocketPeerBase<{
|
||||
class UWSPeer extends WebSocketPeer<{
|
||||
uws: {
|
||||
ws: WebSocket<UserData>;
|
||||
userData: UserData;
|
||||
};
|
||||
}> {
|
||||
_headers: Headers | undefined;
|
||||
_decoder = new TextDecoder();
|
||||
|
||||
get id() {
|
||||
try {
|
||||
const addr = this.ctx.uws.ws?.getRemoteAddressAsText();
|
||||
return new TextDecoder().decode(addr);
|
||||
const addr = this._decoder.decode(
|
||||
this.ctx.uws.ws?.getRemoteAddressAsText(),
|
||||
);
|
||||
return addr.replace(/(0000:)+/, "");
|
||||
} catch {
|
||||
// Error: Invalid access of closed uWS.WebSocket/SSLWebSocket.
|
||||
}
|
||||
|
||||
@@ -1,37 +1,46 @@
|
||||
import type { WebSocketHooks, AdapterHooks, UserHooks } from "./hooks";
|
||||
import { WebSocketPeer } from "./peer";
|
||||
|
||||
export interface CrossWSOptions {}
|
||||
export interface CrossWS extends WebSocketHooks {}
|
||||
|
||||
type AdapterHook = <NAME extends keyof AdapterHooks>(
|
||||
name: NAME,
|
||||
...args: Parameters<AdapterHooks[NAME]>
|
||||
) => ReturnType<AdapterHooks[NAME]>;
|
||||
|
||||
export interface CrossWS extends WebSocketHooks {
|
||||
$: AdapterHook;
|
||||
export interface CrossWSOptions {
|
||||
resolve?: (
|
||||
peer: WebSocketPeer,
|
||||
) => UserHooks | void | Promise<UserHooks | void>;
|
||||
}
|
||||
|
||||
export function createCrossWS(
|
||||
_hooks: UserHooks,
|
||||
options: CrossWSOptions,
|
||||
): CrossWS {
|
||||
const _callHook = options.resolve
|
||||
? async (name: keyof UserHooks, peer: WebSocketPeer, ...args: any[]) => {
|
||||
const hooks = await options.resolve?.(peer);
|
||||
// @ts-expect-error
|
||||
return hooks?.[name]?.(peer, ...args);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
// @ts-expect-error TODO
|
||||
$(name, ...args) {
|
||||
// @ts-expect-error TODO
|
||||
return _hooks[name]?.(...args);
|
||||
$(name, peer, ...args) {
|
||||
_hooks.$?.(name, peer, ...args);
|
||||
_callHook?.(name, peer, ...args);
|
||||
},
|
||||
message(peer, message) {
|
||||
return _hooks.message?.(peer, message);
|
||||
_hooks.message?.(peer, message);
|
||||
_callHook?.("message", peer, message);
|
||||
},
|
||||
open(peer) {
|
||||
return _hooks.open?.(peer);
|
||||
_hooks.open?.(peer);
|
||||
_callHook?.("open", peer);
|
||||
},
|
||||
close(peer, { code, reason }) {
|
||||
return _hooks.close?.(peer, { code, reason });
|
||||
_hooks.close?.(peer, { code, reason });
|
||||
_callHook?.("close", peer, { code, reason });
|
||||
},
|
||||
error(peer, error) {
|
||||
return _hooks.error?.(peer, error);
|
||||
_hooks.error?.(peer, error);
|
||||
_callHook?.("error", peer, error);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
21
src/hooks.ts
21
src/hooks.ts
@@ -1,15 +1,15 @@
|
||||
import { WebSocketError } from "./error";
|
||||
import type { WebSocketMessage } from "./message";
|
||||
import type { WebSocketPeerBase } from "./peer";
|
||||
import type { WebSocketPeer } from "./peer";
|
||||
|
||||
type WSHook<ArgsT extends Array<any> = []> = (
|
||||
peer: WebSocketPeerBase,
|
||||
peer: WebSocketPeer,
|
||||
...args: ArgsT
|
||||
) => void | Promise<void>;
|
||||
|
||||
type WSGlobalHook<ArgsT extends Array<any> = []> = (
|
||||
...args: ArgsT
|
||||
) => void | Promise<void>;
|
||||
// type WSGlobalHook<ArgsT extends Array<any> = []> = (
|
||||
// ...args: ArgsT
|
||||
// ) => void | Promise<void>;
|
||||
|
||||
export type UserHooks = Partial<WebSocketHooks & AdapterHooks>;
|
||||
|
||||
@@ -20,6 +20,9 @@ export function defineWebSocketHooks<T extends UserHooks = UserHooks>(
|
||||
}
|
||||
|
||||
export interface WebSocketHooks {
|
||||
/** Catch-all handler */
|
||||
$: (name: keyof UserHooks, peer: WebSocketPeer, ...args: any[]) => void;
|
||||
|
||||
/** A message is received */
|
||||
message: WSHook<[WebSocketMessage]>;
|
||||
|
||||
@@ -64,10 +67,10 @@ export interface AdapterHooks {
|
||||
"node:pong": WSHook<[data: Buffer]>;
|
||||
"node:unexpected-response": WSHook<[req: any, res: any]>;
|
||||
"node:upgrade": WSHook<[req: any]>;
|
||||
"node:server-error": WSGlobalHook<[error: any]>;
|
||||
"node:server-listening": WSGlobalHook<[]>;
|
||||
"node:server-close": WSGlobalHook<[]>;
|
||||
"node:server-headers": WSGlobalHook<[headers: any, request: any]>;
|
||||
// "node:server-error": WSGlobalHook<[error: any]>;
|
||||
// "node:server-listening": WSGlobalHook<[]>;
|
||||
// "node:server-close": WSGlobalHook<[]>;
|
||||
// "node:server-headers": WSGlobalHook<[headers: any, request: any]>;
|
||||
|
||||
// uws (Node)
|
||||
"uws:open": WSHook<[ws: any]>;
|
||||
|
||||
@@ -8,11 +8,11 @@ const ReadyStateMap = {
|
||||
3: "closed",
|
||||
} as const;
|
||||
|
||||
export abstract class WebSocketPeerBase<AdapterContext = any> {
|
||||
export abstract class WebSocketPeer<AdapterContext = any> {
|
||||
constructor(public ctx: AdapterContext) {}
|
||||
|
||||
get id(): string | undefined {
|
||||
return undefined;
|
||||
return "??";
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
@@ -33,8 +33,7 @@ export abstract class WebSocketPeerBase<AdapterContext = any> {
|
||||
): number;
|
||||
|
||||
toString() {
|
||||
const readyState = ReadyStateMap[this.readyState];
|
||||
return `<WebSocketPeer${this.id ? ` ${this.id}` : ""} (${readyState})>`;
|
||||
return `${this.id || ""}${this.readyState === 1 ? "" : ` [${ReadyStateMap[this.readyState]}]`}`;
|
||||
}
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { expect, it, describe } from "vitest";
|
||||
import {} from "../src";
|
||||
|
||||
describe("crossws", () => {
|
||||
it.todo("pass", () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user