mirror of
https://github.com/nvms/prsm.git
synced 2025-12-16 08:00:53 +00:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d31d083ee3 | ||
|
|
49caf89101 | ||
|
|
1b026ecadb | ||
|
|
48b41c9d19 | ||
|
|
209614c3e8 | ||
|
|
fde46ad338 | ||
|
|
960616b3fb | ||
|
|
c448abfecc | ||
|
|
fe163324df | ||
|
|
21dd9ccf7e | ||
|
|
58980e9f09 | ||
|
|
6e153b1b44 | ||
|
|
978fd71d85 | ||
|
|
9fbd947ad1 | ||
|
|
57af00dc40 | ||
|
|
fb4f275d58 | ||
|
|
c6cb0da27c | ||
|
|
bbd48020de | ||
|
|
66803c1177 | ||
|
|
c492ee8a05 | ||
|
|
0579c0d150 | ||
|
|
36eed400b8 | ||
|
|
9ebef6bdb6 | ||
|
|
1c797eb1ba | ||
|
|
c14dba183c | ||
|
|
d6f237152b | ||
|
|
6181227532 | ||
|
|
6bd9803c61 | ||
|
|
af2cf5a4a4 | ||
|
|
51fc280d8b | ||
|
|
b4751aefe8 | ||
|
|
798164bec0 | ||
|
|
9a835e0c76 | ||
|
|
b25f54ae15 | ||
|
|
9b9c7bea04 | ||
|
|
f2b80feab8 | ||
|
|
9c1370dcbf | ||
|
|
e993afc07f | ||
|
|
663c9ab735 | ||
|
|
f9ccd98d39 | ||
|
|
5a59182775 | ||
|
|
10c18f668e | ||
|
|
8d114a1285 | ||
|
|
f26e2ddbac | ||
|
|
7f7d3168af | ||
|
|
f37f040ecf | ||
|
|
f6c397e1e2 | ||
|
|
31a53fb274 | ||
|
|
2965ecb548 | ||
|
|
7c2850db27 | ||
|
|
0133f59e39 | ||
|
|
06571ac28a | ||
|
|
8db63ab664 | ||
|
|
9140ea34d8 | ||
|
|
22140253fe | ||
|
|
bffefe344a | ||
|
|
8a84f6ea04 | ||
|
|
18f60550e2 | ||
|
|
6fe63c8d58 | ||
|
|
b5cd75a018 | ||
|
|
8af50f0c00 | ||
|
|
5bd827515f | ||
|
|
3395ddb7ac | ||
|
|
ada569c83c | ||
|
|
437c264895 | ||
|
|
7170d1bf89 |
@ -1,2 +1,6 @@
|
||||
node_modules
|
||||
src
|
||||
data
|
||||
tsup.config.ts
|
||||
bump.config.ts
|
||||
bun.lockb
|
||||
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@prsm/express-keepalive-ws",
|
||||
"version": "1.2.7",
|
||||
"version": "1.2.8",
|
||||
"author": "",
|
||||
"main": "./dist/index.js",
|
||||
"devDependencies": {
|
||||
@ -27,6 +27,6 @@
|
||||
"types": "module",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"dependencies": {
|
||||
"@prsm/keepalive-ws": "^0.3.8"
|
||||
"@prsm/keepalive-ws": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,2 +1,6 @@
|
||||
node_modules
|
||||
src
|
||||
tests
|
||||
docker-compose.yml
|
||||
bump.config.ts
|
||||
bun.lockb
|
||||
@ -26,6 +26,9 @@ const server = new KeepAliveServer({
|
||||
port: 8080,
|
||||
pingInterval: 30000,
|
||||
latencyInterval: 5000,
|
||||
// Multi-instance room support (optional):
|
||||
// roomBackend: "redis",
|
||||
// redisOptions: { host: "localhost", port: 6379 }
|
||||
});
|
||||
|
||||
// Register command handlers
|
||||
@ -41,8 +44,8 @@ server.registerCommand("throws", async () => {
|
||||
// Room-based messaging
|
||||
server.registerCommand("join-room", async (context) => {
|
||||
const { roomName } = context.payload;
|
||||
server.addToRoom(roomName, context.connection);
|
||||
server.broadcastRoom(roomName, "user-joined", {
|
||||
await server.addToRoom(roomName, context.connection);
|
||||
await server.broadcastRoom(roomName, "user-joined", {
|
||||
id: context.connection.id
|
||||
});
|
||||
return { success: true };
|
||||
@ -101,17 +104,17 @@ await client.close();
|
||||
|
||||
### Room Management
|
||||
```typescript
|
||||
// Add a connection to a room
|
||||
server.addToRoom("roomName", connection);
|
||||
// Add a connection to a room (async)
|
||||
await server.addToRoom("roomName", connection);
|
||||
|
||||
// Remove a connection from a room
|
||||
server.removeFromRoom("roomName", connection);
|
||||
// Remove a connection from a room (async)
|
||||
await server.removeFromRoom("roomName", connection);
|
||||
|
||||
// Get all connections in a room
|
||||
const roomConnections = server.getRoom("roomName");
|
||||
// Get all connections in a room (async)
|
||||
const roomConnections = await server.getRoom("roomName");
|
||||
|
||||
// Clear all connections from a room
|
||||
server.clearRoom("roomName");
|
||||
// Clear all connections from a room (async)
|
||||
await server.clearRoom("roomName");
|
||||
```
|
||||
|
||||
### Broadcasting
|
||||
@ -162,6 +165,22 @@ server.registerCommand(
|
||||
);
|
||||
```
|
||||
|
||||
## Multi-Instance Room Support
|
||||
|
||||
To enable multi-instance room support (so rooms are shared across all server instances), configure the server with `roomBackend: "redis"` and provide `redisOptions`:
|
||||
|
||||
```typescript
|
||||
import { KeepAliveServer } from "@prsm/keepalive-ws/server";
|
||||
|
||||
const server = new KeepAliveServer({
|
||||
port: 8080,
|
||||
roomBackend: "redis",
|
||||
redisOptions: { host: "localhost", port: 6379 }
|
||||
});
|
||||
```
|
||||
|
||||
All room management methods become async and must be awaited.
|
||||
|
||||
## Graceful Shutdown
|
||||
|
||||
```typescript
|
||||
|
||||
Binary file not shown.
16
packages/keepalive-ws/docker-compose.yml
Normal file
16
packages/keepalive-ws/docker-compose.yml
Normal file
@ -0,0 +1,16 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["redis-server", "--save", "", "--appendonly", "no"]
|
||||
|
||||
redis-commander:
|
||||
image: rediscommander/redis-commander:latest
|
||||
environment:
|
||||
- REDIS_HOSTS=local:redis:6379
|
||||
ports:
|
||||
- "8081:8081"
|
||||
depends_on:
|
||||
- redis
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@prsm/keepalive-ws",
|
||||
"version": "0.3.8",
|
||||
"version": "1.0.3",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "./dist/server/index.js",
|
||||
@ -40,6 +40,7 @@
|
||||
"keywords": [],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ioredis": "^5.6.1",
|
||||
"ws": "^8.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -73,7 +73,21 @@ export class KeepAliveClient extends EventEmitter {
|
||||
private setupConnectionEvents(): void {
|
||||
// Forward relevant events from connection to client
|
||||
this.connection.on("message", (data) => {
|
||||
// Forward the raw message event
|
||||
this.emit("message", data);
|
||||
|
||||
// Also forward the specific command event if it's not a system event
|
||||
// (System events like ping/latency are handled separately below)
|
||||
const systemCommands = [
|
||||
"ping",
|
||||
"pong",
|
||||
"latency",
|
||||
"latency:request",
|
||||
"latency:response",
|
||||
];
|
||||
if (data.command && !systemCommands.includes(data.command)) {
|
||||
this.emit(data.command, data.payload);
|
||||
}
|
||||
});
|
||||
|
||||
this.connection.on("close", () => {
|
||||
@ -150,8 +164,8 @@ export class KeepAliveClient extends EventEmitter {
|
||||
new CodeError(
|
||||
"WebSocket connection error",
|
||||
"ECONNECTION",
|
||||
"ConnectionError",
|
||||
),
|
||||
"ConnectionError"
|
||||
)
|
||||
);
|
||||
};
|
||||
} catch (error) {
|
||||
@ -233,11 +247,12 @@ export class KeepAliveClient extends EventEmitter {
|
||||
|
||||
if (attempt <= this.options.maxReconnectAttempts) {
|
||||
setTimeout(connect, this.options.reconnectInterval);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isReconnecting = false;
|
||||
this._status = Status.OFFLINE;
|
||||
this.emit("reconnectfailed");
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onopen = () => {
|
||||
@ -268,13 +283,13 @@ export class KeepAliveClient extends EventEmitter {
|
||||
command: string,
|
||||
payload?: any,
|
||||
expiresIn: number = 30000,
|
||||
callback?: (result: any, error?: Error) => void,
|
||||
callback?: (result: any, error?: Error) => void
|
||||
): Promise<any> {
|
||||
// Ensure we're connected before sending commands
|
||||
if (this._status !== Status.ONLINE) {
|
||||
return this.connect()
|
||||
.then(() =>
|
||||
this.connection.command(command, payload, expiresIn, callback),
|
||||
this.connection.command(command, payload, expiresIn, callback)
|
||||
)
|
||||
.catch((error) => {
|
||||
if (callback) {
|
||||
|
||||
@ -115,7 +115,7 @@ export class Connection extends EventEmitter {
|
||||
command: string,
|
||||
payload: any,
|
||||
expiresIn: number | null = 30_000,
|
||||
callback?: (result: any, error?: Error) => void,
|
||||
callback?: (result: any, error?: Error) => void
|
||||
): Promise<any> | null {
|
||||
const id = this.ids.reserve();
|
||||
const cmd: Command = { id, command, payload: payload ?? {} };
|
||||
@ -142,17 +142,17 @@ export class Connection extends EventEmitter {
|
||||
|
||||
const timeoutPromise = new Promise<any>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
if (this.callbacks[id]) {
|
||||
if (!this.callbacks[id]) return;
|
||||
|
||||
this.ids.release(id);
|
||||
delete this.callbacks[id];
|
||||
reject(
|
||||
new CodeError(
|
||||
`Command timed out after ${expiresIn}ms.`,
|
||||
"ETIMEOUT",
|
||||
"TimeoutError",
|
||||
),
|
||||
"TimeoutError"
|
||||
)
|
||||
);
|
||||
}
|
||||
}, expiresIn);
|
||||
});
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ export class IdManager {
|
||||
release(id: number) {
|
||||
if (id < 0 || id > this.maxIndex) {
|
||||
throw new TypeError(
|
||||
`ID must be between 0 and ${this.maxIndex}. Got ${id}.`,
|
||||
`ID must be between 0 and ${this.maxIndex}. Got ${id}.`
|
||||
);
|
||||
}
|
||||
this.ids[id] = false;
|
||||
@ -36,7 +36,7 @@ export class IdManager {
|
||||
|
||||
if (this.index === startIndex) {
|
||||
throw new Error(
|
||||
`All IDs are reserved. Make sure to release IDs when they are no longer used.`,
|
||||
`All IDs are reserved. Make sure to release IDs when they are no longer used.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export class Connection extends EventEmitter {
|
||||
constructor(
|
||||
socket: WebSocket,
|
||||
req: IncomingMessage,
|
||||
options: KeepAliveServerOptions,
|
||||
options: KeepAliveServerOptions
|
||||
) {
|
||||
super();
|
||||
this.socket = socket;
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
import { IncomingMessage } from "node:http";
|
||||
import { ServerOptions, WebSocket, WebSocketServer } from "ws";
|
||||
import type { RedisOptions } from "ioredis";
|
||||
import {
|
||||
RoomManager,
|
||||
InMemoryRoomManager,
|
||||
RedisRoomManager,
|
||||
} from "./room-manager";
|
||||
import { CodeError } from "../common/codeerror";
|
||||
import { Command, parseCommand } from "../common/message";
|
||||
import { Status } from "../common/status";
|
||||
@ -34,6 +40,16 @@ export type KeepAliveServerOptions = ServerOptions & {
|
||||
* @default 5000
|
||||
*/
|
||||
latencyInterval?: number;
|
||||
|
||||
/**
|
||||
* Room backend type: "memory" (default) or "redis"
|
||||
*/
|
||||
roomBackend?: "memory" | "redis";
|
||||
|
||||
/**
|
||||
* Redis options, required if roomBackend is "redis"
|
||||
*/
|
||||
redisOptions?: RedisOptions;
|
||||
};
|
||||
|
||||
export class KeepAliveServer extends WebSocketServer {
|
||||
@ -44,7 +60,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
} = {};
|
||||
globalMiddlewares: SocketMiddleware[] = [];
|
||||
middlewares: { [key: string]: SocketMiddleware[] } = {};
|
||||
rooms: { [roomName: string]: Set<string> } = {};
|
||||
roomManager: RoomManager;
|
||||
serverOptions: ServerOptions & {
|
||||
pingInterval: number;
|
||||
latencyInterval: number;
|
||||
@ -67,6 +83,23 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
latencyInterval: opts.latencyInterval ?? 5_000,
|
||||
};
|
||||
|
||||
// Room manager selection
|
||||
if (opts.roomBackend === "redis") {
|
||||
if (!opts.redisOptions) {
|
||||
throw new Error(
|
||||
"redisOptions must be provided when roomBackend is 'redis'"
|
||||
);
|
||||
}
|
||||
this.roomManager = new RedisRoomManager(
|
||||
opts.redisOptions,
|
||||
(id: string) => this.connections[id]
|
||||
);
|
||||
} else {
|
||||
this.roomManager = new InMemoryRoomManager(
|
||||
(id: string) => this.connections[id]
|
||||
);
|
||||
}
|
||||
|
||||
this.on("listening", () => {
|
||||
this._listening = true;
|
||||
this.status = Status.ONLINE;
|
||||
@ -80,14 +113,14 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
this.applyListeners();
|
||||
}
|
||||
|
||||
private cleanupConnection(connection: Connection): void {
|
||||
private async cleanupConnection(connection: Connection): Promise<void> {
|
||||
connection.stopIntervals();
|
||||
delete this.connections[connection.id];
|
||||
|
||||
if (this.remoteAddressToConnections[connection.remoteAddress]) {
|
||||
this.remoteAddressToConnections[connection.remoteAddress] =
|
||||
this.remoteAddressToConnections[connection.remoteAddress].filter(
|
||||
(conn) => conn.id !== connection.id,
|
||||
(conn) => conn.id !== connection.id
|
||||
);
|
||||
|
||||
if (
|
||||
@ -98,9 +131,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
}
|
||||
|
||||
// Remove from all rooms
|
||||
Object.keys(this.rooms).forEach((roomName) => {
|
||||
this.rooms[roomName].delete(connection.id);
|
||||
});
|
||||
await this.roomManager.removeFromAllRooms(connection);
|
||||
}
|
||||
|
||||
private applyListeners(): void {
|
||||
@ -113,13 +144,13 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
}
|
||||
|
||||
this.remoteAddressToConnections[connection.remoteAddress].push(
|
||||
connection,
|
||||
connection
|
||||
);
|
||||
|
||||
this.emit("connected", connection);
|
||||
|
||||
connection.on("close", () => {
|
||||
this.cleanupConnection(connection);
|
||||
connection.on("close", async () => {
|
||||
await this.cleanupConnection(connection);
|
||||
this.emit("close", connection);
|
||||
});
|
||||
|
||||
@ -137,7 +168,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
command.id,
|
||||
command.command,
|
||||
command.payload,
|
||||
connection,
|
||||
connection
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -172,7 +203,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
broadcastRemoteAddress(
|
||||
connection: Connection,
|
||||
command: string,
|
||||
payload: any,
|
||||
payload: any
|
||||
): void {
|
||||
const cmd: Command = { command, payload };
|
||||
const connections =
|
||||
@ -194,47 +225,30 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
* Given a roomName, a command and a payload, broadcasts to all Connections
|
||||
* that are in the room.
|
||||
*/
|
||||
broadcastRoom(roomName: string, command: string, payload: any): void {
|
||||
const cmd: Command = { command, payload };
|
||||
const room = this.rooms[roomName];
|
||||
|
||||
if (!room) return;
|
||||
|
||||
room.forEach((connectionId) => {
|
||||
const connection = this.connections[connectionId];
|
||||
if (connection) {
|
||||
connection.send(cmd);
|
||||
}
|
||||
});
|
||||
async broadcastRoom(
|
||||
roomName: string,
|
||||
command: string,
|
||||
payload: any
|
||||
): Promise<void> {
|
||||
await this.roomManager.broadcastRoom(roomName, command, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a roomName, command, payload, and Connection OR Connection[], broadcasts to all Connections
|
||||
* that are in the room except the provided Connection(s).
|
||||
*/
|
||||
broadcastRoomExclude(
|
||||
async broadcastRoomExclude(
|
||||
roomName: string,
|
||||
command: string,
|
||||
payload: any,
|
||||
connection: Connection | Connection[],
|
||||
): void {
|
||||
const cmd: Command = { command, payload };
|
||||
const room = this.rooms[roomName];
|
||||
|
||||
if (!room) return;
|
||||
|
||||
const excludeIds = Array.isArray(connection)
|
||||
? connection.map((c) => c.id)
|
||||
: [connection.id];
|
||||
|
||||
room.forEach((connectionId) => {
|
||||
if (!excludeIds.includes(connectionId)) {
|
||||
const conn = this.connections[connectionId];
|
||||
if (conn) {
|
||||
conn.send(cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
connection: Connection | Connection[]
|
||||
): Promise<void> {
|
||||
await this.roomManager.broadcastRoomExclude(
|
||||
roomName,
|
||||
command,
|
||||
payload,
|
||||
connection
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -244,7 +258,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
broadcastExclude(
|
||||
connection: Connection,
|
||||
command: string,
|
||||
payload: any,
|
||||
payload: any
|
||||
): void {
|
||||
const cmd: Command = { command, payload };
|
||||
|
||||
@ -258,46 +272,39 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
/**
|
||||
* Add a connection to a room
|
||||
*/
|
||||
addToRoom(roomName: string, connection: Connection): void {
|
||||
this.rooms[roomName] = this.rooms[roomName] ?? new Set();
|
||||
this.rooms[roomName].add(connection.id);
|
||||
async addToRoom(roomName: string, connection: Connection): Promise<void> {
|
||||
await this.roomManager.addToRoom(roomName, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a connection from a room
|
||||
*/
|
||||
removeFromRoom(roomName: string, connection: Connection): void {
|
||||
if (!this.rooms[roomName]) return;
|
||||
this.rooms[roomName].delete(connection.id);
|
||||
async removeFromRoom(
|
||||
roomName: string,
|
||||
connection: Connection
|
||||
): Promise<void> {
|
||||
await this.roomManager.removeFromRoom(roomName, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a connection from all rooms
|
||||
*/
|
||||
removeFromAllRooms(connection: Connection | string): void {
|
||||
const connectionId =
|
||||
typeof connection === "string" ? connection : connection.id;
|
||||
|
||||
Object.keys(this.rooms).forEach((roomName) => {
|
||||
this.rooms[roomName].delete(connectionId);
|
||||
});
|
||||
async removeFromAllRooms(connection: Connection | string): Promise<void> {
|
||||
await this.roomManager.removeFromAllRooms(connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all connections in a room
|
||||
*/
|
||||
getRoom(roomName: string): Connection[] {
|
||||
const ids = this.rooms[roomName] || new Set();
|
||||
return Array.from(ids)
|
||||
.map((id) => this.connections[id])
|
||||
.filter(Boolean);
|
||||
async getRoom(roomName: string): Promise<Connection[]> {
|
||||
return this.roomManager.getRoom(roomName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all connections from a room
|
||||
*/
|
||||
clearRoom(roomName: string): void {
|
||||
this.rooms[roomName] = new Set();
|
||||
async clearRoom(roomName: string): Promise<void> {
|
||||
await this.roomManager.clearRoom(roomName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -306,7 +313,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
async registerCommand<T = any>(
|
||||
command: string,
|
||||
callback: (context: WSContext<any>) => Promise<T> | T,
|
||||
middlewares: SocketMiddleware[] = [],
|
||||
middlewares: SocketMiddleware[] = []
|
||||
): Promise<void> {
|
||||
this.commands[command] = callback;
|
||||
|
||||
@ -322,7 +329,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
*/
|
||||
prependMiddlewareToCommand(
|
||||
command: string,
|
||||
middlewares: SocketMiddleware[],
|
||||
middlewares: SocketMiddleware[]
|
||||
): void {
|
||||
if (middlewares.length) {
|
||||
this.middlewares[command] = this.middlewares[command] || [];
|
||||
@ -335,7 +342,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
*/
|
||||
appendMiddlewareToCommand(
|
||||
command: string,
|
||||
middlewares: SocketMiddleware[],
|
||||
middlewares: SocketMiddleware[]
|
||||
): void {
|
||||
if (middlewares.length) {
|
||||
this.middlewares[command] = this.middlewares[command] || [];
|
||||
@ -350,7 +357,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
id: number,
|
||||
command: string,
|
||||
payload: any,
|
||||
connection: Connection,
|
||||
connection: Connection
|
||||
): Promise<void> {
|
||||
const context = new WSContext(this, connection, payload);
|
||||
|
||||
@ -359,7 +366,7 @@ export class KeepAliveServer extends WebSocketServer {
|
||||
throw new CodeError(
|
||||
`Command [${command}] not found.`,
|
||||
"ENOTFOUND",
|
||||
"CommandError",
|
||||
"CommandError"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
192
packages/keepalive-ws/src/server/room-manager.ts
Normal file
192
packages/keepalive-ws/src/server/room-manager.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import { Connection } from "./connection";
|
||||
import Redis from "ioredis";
|
||||
import type { RedisOptions } from "ioredis";
|
||||
|
||||
export interface RoomManager {
|
||||
addToRoom(roomName: string, connection: Connection): Promise<void>;
|
||||
removeFromRoom(roomName: string, connection: Connection): Promise<void>;
|
||||
removeFromAllRooms(connection: Connection | string): Promise<void>;
|
||||
getRoom(roomName: string): Promise<Connection[]>;
|
||||
clearRoom(roomName: string): Promise<void>;
|
||||
broadcastRoom(roomName: string, command: string, payload: any): Promise<void>;
|
||||
broadcastRoomExclude(
|
||||
roomName: string,
|
||||
command: string,
|
||||
payload: any,
|
||||
connection: Connection | Connection[]
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export class InMemoryRoomManager implements RoomManager {
|
||||
private rooms: { [roomName: string]: Set<string> } = {};
|
||||
private getConnectionById: (id: string) => Connection | undefined;
|
||||
|
||||
constructor(getConnectionById: (id: string) => Connection | undefined) {
|
||||
this.getConnectionById = getConnectionById;
|
||||
}
|
||||
|
||||
async addToRoom(roomName: string, connection: Connection): Promise<void> {
|
||||
this.rooms[roomName] = this.rooms[roomName] ?? new Set();
|
||||
this.rooms[roomName].add(connection.id);
|
||||
}
|
||||
|
||||
async removeFromRoom(
|
||||
roomName: string,
|
||||
connection: Connection
|
||||
): Promise<void> {
|
||||
if (!this.rooms[roomName]) return;
|
||||
this.rooms[roomName].delete(connection.id);
|
||||
}
|
||||
|
||||
async removeFromAllRooms(connection: Connection | string): Promise<void> {
|
||||
const connectionId =
|
||||
typeof connection === "string" ? connection : connection.id;
|
||||
Object.keys(this.rooms).forEach((roomName) => {
|
||||
this.rooms[roomName].delete(connectionId);
|
||||
});
|
||||
}
|
||||
|
||||
async getRoom(roomName: string): Promise<Connection[]> {
|
||||
const ids = this.rooms[roomName] || new Set();
|
||||
return Array.from(ids)
|
||||
.map((id) => this.getConnectionById(id))
|
||||
.filter(Boolean) as Connection[];
|
||||
}
|
||||
|
||||
async clearRoom(roomName: string): Promise<void> {
|
||||
this.rooms[roomName] = new Set();
|
||||
}
|
||||
|
||||
async broadcastRoom(
|
||||
roomName: string,
|
||||
command: string,
|
||||
payload: any
|
||||
): Promise<void> {
|
||||
const ids = this.rooms[roomName];
|
||||
if (!ids) return;
|
||||
for (const connectionId of ids) {
|
||||
const connection = this.getConnectionById(connectionId);
|
||||
if (connection) {
|
||||
connection.send({ command, payload });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async broadcastRoomExclude(
|
||||
roomName: string,
|
||||
command: string,
|
||||
payload: any,
|
||||
connection: Connection | Connection[]
|
||||
): Promise<void> {
|
||||
const ids = this.rooms[roomName];
|
||||
if (!ids) return;
|
||||
const excludeIds = Array.isArray(connection)
|
||||
? connection.map((c) => c.id)
|
||||
: [connection.id];
|
||||
for (const connectionId of ids) {
|
||||
if (!excludeIds.includes(connectionId)) {
|
||||
const conn = this.getConnectionById(connectionId);
|
||||
if (conn) {
|
||||
conn.send({ command, payload });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RedisRoomManager implements RoomManager {
|
||||
private redis: Redis;
|
||||
private getConnectionById: (id: string) => Connection | undefined;
|
||||
|
||||
constructor(
|
||||
redisOptions: RedisOptions,
|
||||
getConnectionById: (id: string) => Connection | undefined
|
||||
) {
|
||||
this.redis = new Redis(redisOptions);
|
||||
this.getConnectionById = getConnectionById;
|
||||
// TODO: reconnect logic?
|
||||
}
|
||||
|
||||
private roomKey(roomName: string) {
|
||||
return `room:${roomName}`;
|
||||
}
|
||||
|
||||
private connRoomsKey(connectionId: string) {
|
||||
return `connection:${connectionId}:rooms`;
|
||||
}
|
||||
|
||||
async addToRoom(roomName: string, connection: Connection): Promise<void> {
|
||||
await this.redis.sadd(this.roomKey(roomName), connection.id);
|
||||
await this.redis.sadd(this.connRoomsKey(connection.id), roomName);
|
||||
}
|
||||
|
||||
async removeFromRoom(
|
||||
roomName: string,
|
||||
connection: Connection
|
||||
): Promise<void> {
|
||||
await this.redis.srem(this.roomKey(roomName), connection.id);
|
||||
await this.redis.srem(this.connRoomsKey(connection.id), roomName);
|
||||
}
|
||||
|
||||
async removeFromAllRooms(connection: Connection | string): Promise<void> {
|
||||
const connectionId =
|
||||
typeof connection === "string" ? connection : connection.id;
|
||||
const roomNames = await this.redis.smembers(
|
||||
this.connRoomsKey(connectionId)
|
||||
);
|
||||
|
||||
if (!(roomNames.length > 0)) return;
|
||||
|
||||
const pipeline = this.redis.pipeline();
|
||||
for (const roomName of roomNames) {
|
||||
pipeline.srem(this.roomKey(roomName), connectionId);
|
||||
}
|
||||
pipeline.del(this.connRoomsKey(connectionId));
|
||||
await pipeline.exec();
|
||||
}
|
||||
|
||||
async getRoom(roomName: string): Promise<Connection[]> {
|
||||
const ids = await this.redis.smembers(this.roomKey(roomName));
|
||||
return ids
|
||||
.map((id) => this.getConnectionById(id))
|
||||
.filter(Boolean) as Connection[];
|
||||
}
|
||||
|
||||
async clearRoom(roomName: string): Promise<void> {
|
||||
await this.redis.del(this.roomKey(roomName));
|
||||
}
|
||||
|
||||
async broadcastRoom(
|
||||
roomName: string,
|
||||
command: string,
|
||||
payload: any
|
||||
): Promise<void> {
|
||||
const ids = await this.redis.smembers(this.roomKey(roomName));
|
||||
for (const connectionId of ids) {
|
||||
const connection = this.getConnectionById(connectionId);
|
||||
if (connection) {
|
||||
connection.send({ command, payload });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async broadcastRoomExclude(
|
||||
roomName: string,
|
||||
command: string,
|
||||
payload: any,
|
||||
connection: Connection | Connection[]
|
||||
): Promise<void> {
|
||||
const ids = await this.redis.smembers(this.roomKey(roomName));
|
||||
const excludeIds = Array.isArray(connection)
|
||||
? connection.map((c) => c.id)
|
||||
: [connection.id];
|
||||
for (const connectionId of ids) {
|
||||
if (!excludeIds.includes(connectionId)) {
|
||||
const conn = this.getConnectionById(connectionId);
|
||||
if (conn) {
|
||||
conn.send({ command, payload });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,13 +2,12 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import { KeepAliveClient, Status } from "../src/client/client";
|
||||
import { KeepAliveServer } from "../src/server/index";
|
||||
|
||||
const createTestServer = (port: number) => {
|
||||
return new KeepAliveServer({
|
||||
const createTestServer = (port: number) =>
|
||||
new KeepAliveServer({
|
||||
port,
|
||||
pingInterval: 1000,
|
||||
latencyInterval: 500,
|
||||
});
|
||||
};
|
||||
|
||||
describe("Advanced KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
const port = 8125;
|
||||
@ -49,14 +48,15 @@ describe("Advanced KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
});
|
||||
|
||||
test("command times out when server doesn't respond", async () => {
|
||||
await server.registerCommand("never-responds", async () => {
|
||||
return new Promise(() => {});
|
||||
});
|
||||
await server.registerCommand(
|
||||
"never-responds",
|
||||
async () => new Promise(() => {})
|
||||
);
|
||||
|
||||
await client.connect();
|
||||
|
||||
await expect(
|
||||
client.command("never-responds", "Should timeout", 500),
|
||||
client.command("never-responds", "Should timeout", 500)
|
||||
).rejects.toThrow(/timed out/);
|
||||
}, 2000);
|
||||
|
||||
@ -82,9 +82,10 @@ describe("Advanced KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
return `Slow: ${context.payload}`;
|
||||
});
|
||||
|
||||
await server.registerCommand("echo", async (context) => {
|
||||
return `Echo: ${context.payload}`;
|
||||
});
|
||||
await server.registerCommand(
|
||||
"echo",
|
||||
async (context) => `Echo: ${context.payload}`
|
||||
);
|
||||
|
||||
await client.connect();
|
||||
|
||||
@ -98,9 +99,7 @@ describe("Advanced KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
}, 3000);
|
||||
|
||||
test("handles large payloads correctly", async () => {
|
||||
await server.registerCommand("echo", async (context) => {
|
||||
return context.payload;
|
||||
});
|
||||
await server.registerCommand("echo", async (context) => context.payload);
|
||||
|
||||
await client.connect();
|
||||
|
||||
@ -123,9 +122,10 @@ describe("Advanced KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
}, 10000);
|
||||
|
||||
test("server handles multiple client connections", async () => {
|
||||
await server.registerCommand("echo", async (context) => {
|
||||
return `Echo: ${context.payload}`;
|
||||
});
|
||||
await server.registerCommand(
|
||||
"echo",
|
||||
async (context) => `Echo: ${context.payload}`
|
||||
);
|
||||
|
||||
const clients = Array(5)
|
||||
.fill(0)
|
||||
@ -134,7 +134,7 @@ describe("Advanced KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
await Promise.all(clients.map((client) => client.connect()));
|
||||
|
||||
const results = await Promise.all(
|
||||
clients.map((client, i) => client.command("echo", `Client ${i}`, 1000)),
|
||||
clients.map((client, i) => client.command("echo", `Client ${i}`, 1000))
|
||||
);
|
||||
|
||||
results.forEach((result, i) => {
|
||||
|
||||
@ -2,13 +2,12 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import { KeepAliveClient, Status } from "../src/client/client";
|
||||
import { KeepAliveServer } from "../src/server/index";
|
||||
|
||||
const createTestServer = (port: number) => {
|
||||
return new KeepAliveServer({
|
||||
const createTestServer = (port: number) =>
|
||||
new KeepAliveServer({
|
||||
port,
|
||||
pingInterval: 1000,
|
||||
latencyInterval: 500,
|
||||
});
|
||||
};
|
||||
|
||||
describe("Basic KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
const port = 8124;
|
||||
@ -48,18 +47,17 @@ describe("Basic KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
});
|
||||
|
||||
test("client-server connection should be online", async () => {
|
||||
await server.registerCommand("echo", async (context) => {
|
||||
return context.payload;
|
||||
});
|
||||
await server.registerCommand("echo", async (context) => context.payload);
|
||||
|
||||
await client.connect();
|
||||
expect(client.status).toBe(Status.ONLINE);
|
||||
}, 10000);
|
||||
|
||||
test("simple echo command", async () => {
|
||||
await server.registerCommand("echo", async (context) => {
|
||||
return `Echo: ${context.payload}`;
|
||||
});
|
||||
await server.registerCommand(
|
||||
"echo",
|
||||
async (context) => `Echo: ${context.payload}`
|
||||
);
|
||||
|
||||
await client.connect();
|
||||
|
||||
@ -68,9 +66,7 @@ describe("Basic KeepAliveClient and KeepAliveServer Tests", () => {
|
||||
}, 10000);
|
||||
|
||||
test("connect should resolve when already connected", async () => {
|
||||
await server.registerCommand("echo", async (context) => {
|
||||
return context.payload;
|
||||
});
|
||||
await server.registerCommand("echo", async (context) => context.payload);
|
||||
|
||||
await client.connect();
|
||||
expect(client.status).toBe(Status.ONLINE);
|
||||
|
||||
394
packages/keepalive-ws/tests/redis-room.test.ts
Normal file
394
packages/keepalive-ws/tests/redis-room.test.ts
Normal file
@ -0,0 +1,394 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import Redis from "ioredis";
|
||||
import { KeepAliveClient, Status } from "../src/client/client";
|
||||
import { KeepAliveServer } from "../src/server/index";
|
||||
|
||||
const REDIS_HOST = process.env.REDIS_HOST || "127.0.0.1";
|
||||
const REDIS_PORT = process.env.REDIS_PORT
|
||||
? parseInt(process.env.REDIS_PORT, 10)
|
||||
: 6379;
|
||||
|
||||
const createRedisServer = (port: number) =>
|
||||
new KeepAliveServer({
|
||||
port,
|
||||
pingInterval: 1000,
|
||||
latencyInterval: 500,
|
||||
roomBackend: "redis",
|
||||
redisOptions: { host: REDIS_HOST, port: REDIS_PORT },
|
||||
});
|
||||
|
||||
const flushRedis = async () => {
|
||||
const redis = new Redis({ host: REDIS_HOST, port: REDIS_PORT });
|
||||
await redis.flushdb();
|
||||
await redis.quit();
|
||||
};
|
||||
|
||||
describe("KeepAliveServer with Redis room backend", () => {
|
||||
const port = 8126;
|
||||
let server: KeepAliveServer;
|
||||
let clientA: KeepAliveClient;
|
||||
let clientB: KeepAliveClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
await flushRedis();
|
||||
|
||||
server = createRedisServer(port);
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.on("listening", () => resolve());
|
||||
if (server.listening) resolve();
|
||||
});
|
||||
|
||||
clientA = new KeepAliveClient(`ws://localhost:${port}`);
|
||||
clientB = new KeepAliveClient(`ws://localhost:${port}`);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (clientA.status === Status.ONLINE) await clientA.close();
|
||||
if (clientB.status === Status.ONLINE) await clientB.close();
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
if (server) {
|
||||
server.close(() => resolve());
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test("multi-instance room membership and broadcast with Redis", async () => {
|
||||
await server.registerCommand("join-room", async (context) => {
|
||||
await server.addToRoom(context.payload.room, context.connection);
|
||||
return { joined: true };
|
||||
});
|
||||
|
||||
await server.registerCommand("broadcast-room", async (context) => {
|
||||
await server.broadcastRoom(
|
||||
context.payload.room,
|
||||
"room-message",
|
||||
context.payload.message
|
||||
);
|
||||
return { sent: true };
|
||||
});
|
||||
|
||||
await clientA.connect();
|
||||
await clientB.connect();
|
||||
|
||||
let receivedA = false;
|
||||
let receivedB = false;
|
||||
|
||||
clientA.on("room-message", (data) => {
|
||||
if (data === "hello") receivedA = true;
|
||||
});
|
||||
clientB.on("room-message", (data) => {
|
||||
if (data === "hello") receivedB = true;
|
||||
});
|
||||
|
||||
await clientA.command("join-room", { room: "testroom" });
|
||||
await clientB.command("join-room", { room: "testroom" });
|
||||
|
||||
await clientA.command("broadcast-room", {
|
||||
room: "testroom",
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
// Wait for both events or timeout
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(resolve, 2000);
|
||||
const check = () => {
|
||||
if (receivedA && receivedB) {
|
||||
clearTimeout(timeout);
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
clientA.on("room-message", check);
|
||||
clientB.on("room-message", check);
|
||||
});
|
||||
|
||||
expect(receivedA).toBe(true);
|
||||
expect(receivedB).toBe(true);
|
||||
}, 10000);
|
||||
|
||||
test("removeFromRoom removes a client from a specific room", async () => {
|
||||
await server.registerCommand("join-room", async (context) => {
|
||||
await server.addToRoom(context.payload.room, context.connection);
|
||||
return { joined: true };
|
||||
});
|
||||
await server.registerCommand("leave-room", async (context) => {
|
||||
await server.removeFromRoom(context.payload.room, context.connection);
|
||||
return { left: true };
|
||||
});
|
||||
await server.registerCommand("broadcast-room", async (context) => {
|
||||
await server.broadcastRoom(
|
||||
context.payload.room,
|
||||
"room-message",
|
||||
context.payload.message
|
||||
);
|
||||
return { sent: true };
|
||||
});
|
||||
|
||||
await clientA.connect();
|
||||
await clientB.connect();
|
||||
|
||||
let receivedA = false;
|
||||
let receivedB = false;
|
||||
|
||||
clientA.on("room-message", (data) => {
|
||||
if (data === "hello after leave") receivedA = true;
|
||||
});
|
||||
clientB.on("room-message", (data) => {
|
||||
if (data === "hello after leave") receivedB = true;
|
||||
});
|
||||
|
||||
await clientA.command("join-room", { room: "testroom-leave" });
|
||||
await clientB.command("join-room", { room: "testroom-leave" });
|
||||
|
||||
// Ensure both are in before leaving
|
||||
await new Promise((res) => setTimeout(res, 100)); // Short delay for redis propagation
|
||||
|
||||
await clientA.command("leave-room", { room: "testroom-leave" });
|
||||
|
||||
// Wait a bit for leave command to process
|
||||
await new Promise((res) => setTimeout(res, 100));
|
||||
|
||||
await clientB.command("broadcast-room", {
|
||||
room: "testroom-leave",
|
||||
message: "hello after leave",
|
||||
});
|
||||
|
||||
// Wait for potential message or timeout
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
expect(receivedA).toBe(false); // Client A should not receive the message
|
||||
expect(receivedB).toBe(true); // Client B should receive the message
|
||||
}, 10000);
|
||||
|
||||
test("removeFromAllRooms removes a client from all rooms", async () => {
|
||||
await server.registerCommand("join-room", async (context) => {
|
||||
await server.addToRoom(context.payload.room, context.connection);
|
||||
return { joined: true };
|
||||
});
|
||||
await server.registerCommand("leave-all-rooms", async (context) => {
|
||||
await server.removeFromAllRooms(context.connection);
|
||||
return { left_all: true };
|
||||
});
|
||||
await server.registerCommand("broadcast-room", async (context) => {
|
||||
await server.broadcastRoom(
|
||||
context.payload.room,
|
||||
"room-message",
|
||||
context.payload.message
|
||||
);
|
||||
return { sent: true };
|
||||
});
|
||||
|
||||
await clientA.connect();
|
||||
await clientB.connect();
|
||||
|
||||
let receivedA_room1 = false;
|
||||
let receivedA_room2 = false;
|
||||
let receivedB_room1 = false;
|
||||
|
||||
clientA.on("room-message", (data) => {
|
||||
if (data === "hello room1 after all") receivedA_room1 = true;
|
||||
if (data === "hello room2 after all") receivedA_room2 = true;
|
||||
});
|
||||
clientB.on("room-message", (data) => {
|
||||
if (data === "hello room1 after all") receivedB_room1 = true;
|
||||
});
|
||||
|
||||
await clientA.command("join-room", { room: "room1" });
|
||||
await clientA.command("join-room", { room: "room2" });
|
||||
await clientB.command("join-room", { room: "room1" });
|
||||
|
||||
// Ensure joins are processed
|
||||
await new Promise((res) => setTimeout(res, 100));
|
||||
|
||||
await clientA.command("leave-all-rooms", {});
|
||||
|
||||
// Wait a bit for leave command to process
|
||||
await new Promise((res) => setTimeout(res, 100));
|
||||
|
||||
// Broadcast to room1
|
||||
await clientB.command("broadcast-room", {
|
||||
room: "room1",
|
||||
message: "hello room1 after all",
|
||||
});
|
||||
// Broadcast to room2 (no one should be left)
|
||||
await clientB.command("broadcast-room", {
|
||||
// Client B isn't in room2, but can still broadcast
|
||||
room: "room2",
|
||||
message: "hello room2 after all",
|
||||
});
|
||||
|
||||
// Wait for potential messages or timeout
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
expect(receivedA_room1).toBe(false); // Client A should not receive from room1
|
||||
expect(receivedA_room2).toBe(false); // Client A should not receive from room2
|
||||
expect(receivedB_room1).toBe(true); // Client B should receive from room1
|
||||
}, 10000);
|
||||
|
||||
test("clearRoom removes all clients from a room", async () => {
|
||||
await server.registerCommand("join-room", async (context) => {
|
||||
await server.addToRoom(context.payload.room, context.connection);
|
||||
return { joined: true };
|
||||
});
|
||||
await server.registerCommand("clear-room", async (context) => {
|
||||
await server.clearRoom(context.payload.room);
|
||||
return { cleared: true };
|
||||
});
|
||||
await server.registerCommand("broadcast-room", async (context) => {
|
||||
await server.broadcastRoom(
|
||||
context.payload.room,
|
||||
"room-message",
|
||||
context.payload.message
|
||||
);
|
||||
return { sent: true };
|
||||
});
|
||||
|
||||
await clientA.connect();
|
||||
await clientB.connect();
|
||||
|
||||
let receivedA = false;
|
||||
let receivedB = false;
|
||||
|
||||
clientA.on("room-message", (data) => {
|
||||
if (data === "hello after clear") receivedA = true;
|
||||
});
|
||||
clientB.on("room-message", (data) => {
|
||||
if (data === "hello after clear") receivedB = true;
|
||||
});
|
||||
|
||||
await clientA.command("join-room", { room: "testroom-clear" });
|
||||
await clientB.command("join-room", { room: "testroom-clear" });
|
||||
|
||||
// Ensure joins are processed
|
||||
await new Promise((res) => setTimeout(res, 100));
|
||||
|
||||
await clientA.command("clear-room", { room: "testroom-clear" });
|
||||
|
||||
// Wait a bit for clear command to process
|
||||
await new Promise((res) => setTimeout(res, 100));
|
||||
|
||||
// Try broadcasting (client A is still connected, just not in room)
|
||||
await clientA.command("broadcast-room", {
|
||||
room: "testroom-clear",
|
||||
message: "hello after clear",
|
||||
});
|
||||
|
||||
// Wait for potential messages or timeout
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
expect(receivedA).toBe(false); // Client A should not receive
|
||||
expect(receivedB).toBe(false); // Client B should not receive
|
||||
}, 10000);
|
||||
|
||||
test("broadcastRoomExclude sends to all except specified clients", async () => {
|
||||
const clientC = new KeepAliveClient(`ws://localhost:${port}`);
|
||||
|
||||
await server.registerCommand("join-room", async (context) => {
|
||||
await server.addToRoom(context.payload.room, context.connection);
|
||||
return { joined: true };
|
||||
});
|
||||
await server.registerCommand("broadcast-exclude", async (context) => {
|
||||
await server.broadcastRoomExclude(
|
||||
context.payload.room,
|
||||
"room-message",
|
||||
context.payload.message,
|
||||
context.connection // Exclude sender
|
||||
);
|
||||
return { sent_exclude: true };
|
||||
});
|
||||
|
||||
await clientA.connect();
|
||||
await clientB.connect();
|
||||
await clientC.connect();
|
||||
|
||||
let receivedA = false;
|
||||
let receivedB = false;
|
||||
let receivedC = false;
|
||||
|
||||
clientA.on("room-message", (data) => {
|
||||
if (data === "hello exclude") receivedA = true;
|
||||
});
|
||||
clientB.on("room-message", (data) => {
|
||||
if (data === "hello exclude") receivedB = true;
|
||||
});
|
||||
clientC.on("room-message", (data) => {
|
||||
if (data === "hello exclude") receivedC = true;
|
||||
});
|
||||
|
||||
await clientA.command("join-room", { room: "testroom-exclude" });
|
||||
await clientB.command("join-room", { room: "testroom-exclude" });
|
||||
await clientC.command("join-room", { room: "testroom-exclude" });
|
||||
|
||||
// Ensure joins are processed
|
||||
await new Promise((res) => setTimeout(res, 100));
|
||||
|
||||
// Client A broadcasts, excluding itself
|
||||
await clientA.command("broadcast-exclude", {
|
||||
room: "testroom-exclude",
|
||||
message: "hello exclude",
|
||||
});
|
||||
|
||||
// Wait for potential messages or timeout
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
expect(receivedA).toBe(false); // Client A (sender) should not receive
|
||||
expect(receivedB).toBe(true); // Client B should receive
|
||||
expect(receivedC).toBe(true); // Client C should receive
|
||||
|
||||
if (clientC.status === Status.ONLINE) await clientC.close();
|
||||
}, 10000);
|
||||
|
||||
test("getRoom correctly retrieves all connections in a room", async () => {
|
||||
await server.registerCommand("join-room", async (context) => {
|
||||
await server.addToRoom(context.payload.room, context.connection);
|
||||
return { joined: true, id: context.connection.id };
|
||||
});
|
||||
|
||||
await server.registerCommand("get-room-members", async (context) => {
|
||||
const connections = await server.getRoom(context.payload.room);
|
||||
return {
|
||||
count: connections.length,
|
||||
memberIds: connections.map((conn) => conn.id),
|
||||
};
|
||||
});
|
||||
|
||||
await clientA.connect();
|
||||
await clientB.connect();
|
||||
|
||||
const { id: clientAID } = await clientA.command("join-room", {
|
||||
room: "test-get-room-1",
|
||||
});
|
||||
const { id: clientBID } = await clientB.command("join-room", {
|
||||
room: "test-get-room-1",
|
||||
});
|
||||
await clientA.command("join-room", { room: "test-get-room-2" });
|
||||
|
||||
await new Promise((res) => setTimeout(res, 200));
|
||||
|
||||
const room1Result = await clientA.command("get-room-members", {
|
||||
room: "test-get-room-1",
|
||||
});
|
||||
|
||||
const room2Result = await clientA.command("get-room-members", {
|
||||
room: "test-get-room-2",
|
||||
});
|
||||
const emptyRoomResult = await clientA.command("get-room-members", {
|
||||
room: "non-existent-room",
|
||||
});
|
||||
|
||||
expect(room1Result.count).toBe(2);
|
||||
expect(room1Result.memberIds.length).toBe(2);
|
||||
expect(room1Result.memberIds).toContain(clientAID);
|
||||
expect(room1Result.memberIds).toContain(clientBID);
|
||||
|
||||
expect(room2Result.count).toBe(1);
|
||||
expect(room2Result.memberIds.length).toBe(1);
|
||||
expect(room2Result.memberIds).toContain(clientAID);
|
||||
|
||||
expect(emptyRoomResult.count).toBe(0);
|
||||
expect(emptyRoomResult.memberIds).toEqual([]);
|
||||
}, 10000);
|
||||
});
|
||||
34
packages/mesh-express/.gitignore
vendored
Normal file
34
packages/mesh-express/.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
6
packages/mesh-express/.npmignore
Normal file
6
packages/mesh-express/.npmignore
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
src
|
||||
docker-compose.yml
|
||||
bun.lock
|
||||
vitest.config.ts
|
||||
TODO.md
|
||||
121
packages/mesh-express/README.md
Normal file
121
packages/mesh-express/README.md
Normal file
@ -0,0 +1,121 @@
|
||||
# @prsm/mesh-express
|
||||
|
||||
A simple adapter for running [Mesh](https://github.com/node-prism/mesh) inside an existing Express + HTTP server.
|
||||
|
||||
This package wires up a `MeshServer` instance to handle WebSocket upgrades using the native `upgrade` event and exposes an optional Express middleware.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @prsm/mesh-express
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import express from "express";
|
||||
import http from "http";
|
||||
import createMeshMiddleware from "@prsm/mesh-express";
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
|
||||
const { middleware, mesh } = createMeshMiddleware(server, {
|
||||
path: "/ws",
|
||||
redisOptions: { host: "localhost", port: 6379 },
|
||||
});
|
||||
|
||||
app.use(middleware); // optional
|
||||
|
||||
mesh.registerCommand("echo", async (ctx) => {
|
||||
return `echo: ${ctx.payload}`;
|
||||
});
|
||||
|
||||
server.listen(3000, () => {
|
||||
console.log("Server listening on port 3000");
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What does the middleware do?
|
||||
|
||||
The middleware enables Express to recognize WebSocket upgrade requests and adds a `.ws()` method to the request object. While most upgrades are handled automatically by Mesh via the `upgrade` event, `.ws()` gives you manual control for custom upgrade logic (e.g. auth).
|
||||
|
||||
## When to use `.ws()`
|
||||
|
||||
Use `.ws()` when you need to *conditionally accept or reject* WebSocket connections inside an Express route.
|
||||
|
||||
```ts
|
||||
app.use("/ws", (req, res, next) => {
|
||||
if (!req.ws) return next();
|
||||
|
||||
const token = req.query.token;
|
||||
|
||||
if (!isValidToken(token)) {
|
||||
return res.status(401).send("Unauthorized");
|
||||
}
|
||||
|
||||
const ws = await req.ws(); // manually upgrade
|
||||
|
||||
ws.send("Upgraded!");
|
||||
});
|
||||
```
|
||||
|
||||
This is useful for:
|
||||
|
||||
- Auth checks during upgrade
|
||||
- Inspecting query params or headers
|
||||
- Rejecting based on app state (e.g. maintenance)
|
||||
|
||||
In most cases, you won't need `.ws()`—Mesh handles upgrades automatically if the request path matches, but the option is there when you need it.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### `createMeshMiddleware(server, options)`
|
||||
|
||||
| Param | Type | Description |
|
||||
|-----------|------------------------|------------------------------------------|
|
||||
| `server` | `http.Server` | The existing HTTP server to attach to |
|
||||
| `options` | `MeshServerOptions` | Standard Mesh config (plus optional `path`) |
|
||||
|
||||
Returns an object with:
|
||||
|
||||
- `middleware`: an Express-compatible async middleware
|
||||
- `mesh`: the `MeshServer` instance for command registration, etc.
|
||||
|
||||
---
|
||||
|
||||
## Client usage
|
||||
|
||||
On the client, use the standard Mesh client:
|
||||
|
||||
```ts
|
||||
import { MeshClient } from "@prsm/mesh/client";
|
||||
|
||||
const client = new MeshClient("ws://localhost:3000/ws");
|
||||
await client.connect();
|
||||
|
||||
const res = await client.command("echo", "hello");
|
||||
console.log(res); // "echo: hello"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Defaults to using `/` as the WebSocket upgrade path if `options.path` is not specified.
|
||||
- If the request does not match the configured path, the socket is rejected with HTTP 400.
|
||||
- This package does **not** create a server—it binds Mesh to your existing one.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
326
packages/mesh-express/bun.lock
Normal file
326
packages/mesh-express/bun.lock
Normal file
@ -0,0 +1,326 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "mesh-express",
|
||||
"dependencies": {
|
||||
"@prsm/mesh": "^1.0.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsup": "^8.4.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.2", "", { "os": "android", "cpu": "arm" }, "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.2", "", { "os": "android", "cpu": "x64" }, "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.2", "", { "os": "linux", "cpu": "arm" }, "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.2", "", { "os": "linux", "cpu": "x64" }, "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.2", "", { "os": "none", "cpu": "arm64" }, "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.2", "", { "os": "none", "cpu": "x64" }, "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="],
|
||||
|
||||
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@prsm/mesh": ["@prsm/mesh@1.0.4", "", { "dependencies": { "deasync": "^0.1.30", "fast-json-patch": "^3.1.1", "ioredis": "^5.6.1", "uuid": "^11.1.0", "ws": "^8.18.1" }, "peerDependencies": { "typescript": "^5.8.3" } }, "sha512-3IzdNu06GMgj/A2P0XcaW849p5K4J+vGTNJBY0OwiDLEiOLRa+07Zs6gDiUoOyFEK4vllZhJRSz+iiMaqpyWSw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.10", "", { "dependencies": { "bun-types": "1.2.10" } }, "sha512-eilv6WFM3M0c9ztJt7/g80BDusK98z/FrFwseZgT4bXCq2vPhXD4z8R3oddmAn+R/Nmz9vBn4kweJKmGTZj+lg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="],
|
||||
|
||||
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"deasync": ["deasync@0.1.30", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^1.7.1" } }, "sha512-OaAjvEQuQ9tJsKG4oHO9nV1UHTwb2Qc2+fadB0VeVtD0Z9wiG1XPGLJ4W3aLhAoQSYTaLROFRbd5X20Dkzf7MQ=="],
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.2", "@esbuild/android-arm": "0.25.2", "@esbuild/android-arm64": "0.25.2", "@esbuild/android-x64": "0.25.2", "@esbuild/darwin-arm64": "0.25.2", "@esbuild/darwin-x64": "0.25.2", "@esbuild/freebsd-arm64": "0.25.2", "@esbuild/freebsd-x64": "0.25.2", "@esbuild/linux-arm": "0.25.2", "@esbuild/linux-arm64": "0.25.2", "@esbuild/linux-ia32": "0.25.2", "@esbuild/linux-loong64": "0.25.2", "@esbuild/linux-mips64el": "0.25.2", "@esbuild/linux-ppc64": "0.25.2", "@esbuild/linux-riscv64": "0.25.2", "@esbuild/linux-s390x": "0.25.2", "@esbuild/linux-x64": "0.25.2", "@esbuild/netbsd-arm64": "0.25.2", "@esbuild/netbsd-x64": "0.25.2", "@esbuild/openbsd-arm64": "0.25.2", "@esbuild/openbsd-x64": "0.25.2", "@esbuild/sunos-x64": "0.25.2", "@esbuild/win32-arm64": "0.25.2", "@esbuild/win32-ia32": "0.25.2", "@esbuild/win32-x64": "0.25.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ=="],
|
||||
|
||||
"fast-json-patch": ["fast-json-patch@3.1.1", "", {}, "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ=="],
|
||||
|
||||
"fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
|
||||
|
||||
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
|
||||
|
||||
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
|
||||
|
||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||
|
||||
"load-tsconfig": ["load-tsconfig@0.2.5", "", {}, "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
|
||||
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
|
||||
|
||||
"lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||
|
||||
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
|
||||
|
||||
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
|
||||
|
||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||
|
||||
"rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="],
|
||||
|
||||
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
|
||||
|
||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
|
||||
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="],
|
||||
|
||||
"tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="],
|
||||
|
||||
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
|
||||
|
||||
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
|
||||
|
||||
"tsup": ["tsup@8.4.0", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.25.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "0.8.0-beta.0", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-b+eZbPCjz10fRryaAA7C8xlIHnf8VnsaRqydheLIqwG/Mcpfk8Z5zp3HayX7GaTygkigHl5cBUs+IhcySiIexQ=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
27
packages/mesh-express/package.json
Normal file
27
packages/mesh-express/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@prsm/mesh-express",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsup"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prsm/mesh": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsup": "^8.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
72
packages/mesh-express/src/index.ts
Normal file
72
packages/mesh-express/src/index.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { STATUS_CODES, type Server as HTTPServer } from "node:http";
|
||||
import { MeshServer, type MeshServerOptions } from "@prsm/mesh/server";
|
||||
|
||||
type Middleware = (req, res, next) => Promise<void>;
|
||||
|
||||
interface MeshExpressResult {
|
||||
middleware: Middleware;
|
||||
mesh: MeshServer;
|
||||
}
|
||||
|
||||
const createMeshMiddleware = (
|
||||
server: HTTPServer,
|
||||
options: MeshServerOptions
|
||||
): MeshExpressResult => {
|
||||
const path = options.path || "/";
|
||||
const mesh = new MeshServer({ ...options, noServer: true });
|
||||
|
||||
server.on("upgrade", (request, socket, head) => {
|
||||
const { pathname } = new URL(
|
||||
request.url || "",
|
||||
`http://${request.headers.host}`
|
||||
);
|
||||
|
||||
if (pathname !== path) {
|
||||
socket.write(
|
||||
[
|
||||
`HTTP/1.1 400 ${STATUS_CODES[400]}`,
|
||||
"Connection: close",
|
||||
"Content-Type: text/plain",
|
||||
`Content-Length: ${Buffer.byteLength(STATUS_CODES[400])}`,
|
||||
"",
|
||||
STATUS_CODES[400],
|
||||
].join("\r\n")
|
||||
);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
mesh.handleUpgrade(request, socket, head, (client, req) => {
|
||||
mesh.emit("connection", client, req);
|
||||
});
|
||||
});
|
||||
|
||||
const middleware: Middleware = async (req, res, next) => {
|
||||
const upgradeHeader =
|
||||
req.headers.upgrade
|
||||
?.toLowerCase()
|
||||
.split(",")
|
||||
.map((s) => s.trim()) || [];
|
||||
|
||||
if (upgradeHeader.includes("websocket")) {
|
||||
req.ws = () =>
|
||||
new Promise((resolve) => {
|
||||
mesh.handleUpgrade(req, req.socket, Buffer.alloc(0), (client) => {
|
||||
mesh.emit("connection", client, req);
|
||||
resolve(client);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
|
||||
return { middleware, mesh };
|
||||
};
|
||||
|
||||
export default createMeshMiddleware;
|
||||
export {
|
||||
MeshServer,
|
||||
type MeshServerOptions,
|
||||
type MeshContext,
|
||||
} from "@prsm/mesh/server";
|
||||
28
packages/mesh-express/tsconfig.json
Normal file
28
packages/mesh-express/tsconfig.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["esnext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": false,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
11
packages/mesh-express/tsup.config.ts
Normal file
11
packages/mesh-express/tsup.config.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["cjs", "esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
minify: true,
|
||||
sourcemap: "inline",
|
||||
target: "esnext",
|
||||
});
|
||||
4
packages/mesh/README.md
Normal file
4
packages/mesh/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
> [!NOTE]
|
||||
> This package has been moved to its own organization and can be found at:
|
||||
>
|
||||
> https://github.com/mesh-kit/core
|
||||
Loading…
Reference in New Issue
Block a user