@hazejs/socket.io 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +28 -2
- package/dist/index.d.mts +81 -0
- package/dist/index.mjs +204 -0
- package/package.json +66 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present hazejs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hazejs/socket.io
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Socket.IO server integration for hazejs: namespaces from `app/socket/*.ts`, guard-based handshake auth, Redis adapter, Node and Bun.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @hazejs/socket.io socket.io
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// app/socket/chat.ts
|
|
15
|
+
export default defineNamespace({
|
|
16
|
+
middleware: [
|
|
17
|
+
authenticate()
|
|
18
|
+
],
|
|
19
|
+
connection(socket, io) {
|
|
20
|
+
socket.on('chat:send', text => io.of('/chat').emit('chat:message', text))
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Part of [hazejs](https://github.com/penna-org/haze) — see the main README for documentation.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
|
|
29
|
+
[MIT](./LICENSE)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ConfigInput, ServiceProvider } from "@hazejs/core";
|
|
2
|
+
import { Namespace, Server, ServerOptions, Socket } from "socket.io";
|
|
3
|
+
declare const kNamespace: unique symbol;
|
|
4
|
+
export interface SocketIOConfig {
|
|
5
|
+
/** 路径,默认 `/socket.io/`(与 socket.io-client 的默认值一致) */
|
|
6
|
+
path?: string;
|
|
7
|
+
/** CORS,默认沿用 config/cors.ts 的 origin;false 关闭(同源) */
|
|
8
|
+
cors?: ServerOptions['cors'] | false;
|
|
9
|
+
/** 多实例:官方 @socket.io/redis-adapter(需要 ioredis) */
|
|
10
|
+
adapter?: {
|
|
11
|
+
driver: 'redis';
|
|
12
|
+
url?: string;
|
|
13
|
+
key?: string;
|
|
14
|
+
};
|
|
15
|
+
/** 透传给 socket.io Server 的其它选项(pingInterval、connectionStateRecovery 等) */
|
|
16
|
+
options?: Partial<ServerOptions>;
|
|
17
|
+
}
|
|
18
|
+
/** `config/socketio.ts` 的默认导出 */
|
|
19
|
+
export declare function defineSocketIOConfig(config: SocketIOConfig | (() => SocketIOConfig)): ConfigInput<SocketIOConfig>;
|
|
20
|
+
export type SocketMiddleware = (socket: Socket, next: (error?: Error) => void) => unknown;
|
|
21
|
+
export interface NamespaceDefinition {
|
|
22
|
+
readonly [kNamespace]: true;
|
|
23
|
+
/** 命名空间,默认由文件名决定:index → `/`,chat → `/chat` */
|
|
24
|
+
readonly name?: string;
|
|
25
|
+
readonly middleware: SocketMiddleware[];
|
|
26
|
+
readonly connection: (socket: Socket, io: Server, namespace: Namespace) => unknown;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 定义命名空间(app/socket/*.ts 的默认导出,自动注册):
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* export default defineNamespace({
|
|
33
|
+
* middleware: [authenticate()],
|
|
34
|
+
* connection(socket, io) {
|
|
35
|
+
* socket.on('chat:send', (text, ack) => { io.of('/chat').emit('chat:message', text); ack?.('ok') })
|
|
36
|
+
* },
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function defineNamespace(definition: {
|
|
41
|
+
name?: string;
|
|
42
|
+
middleware?: SocketMiddleware[];
|
|
43
|
+
connection: NamespaceDefinition['connection'];
|
|
44
|
+
}): NamespaceDefinition;
|
|
45
|
+
export declare function isNamespace(value: unknown): value is NamespaceDefinition;
|
|
46
|
+
/** 注入令牌:`inject(SocketIOServer)` 取得 socket.io 的 Server */
|
|
47
|
+
export declare const SocketIOServer: import("@hazejs/core").Token<Server<import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, import("socket.io").DefaultEventsMap, any>>;
|
|
48
|
+
/**
|
|
49
|
+
* Socket.IO 服务提供者(app/provider/SocketProvider.ts:`export default socketIoProvider()`):
|
|
50
|
+
* 创建 socket.io Server(跨热更新保留),注册 app/socket 下的命名空间,挂到框架的 HTTP 服务器。
|
|
51
|
+
* 新连接总是使用最新的命名空间定义;Edge / Serverless 运行时上不启动。
|
|
52
|
+
*/
|
|
53
|
+
export declare function socketIoProvider(): ServiceProvider;
|
|
54
|
+
/**
|
|
55
|
+
* 握手鉴权:按 config/auth.ts 的 Guard 识别用户,存入 `socket.data.user`。
|
|
56
|
+
* 令牌取 `io({ auth: { token } })` 或 Authorization 头;Session Guard 读握手请求的 Cookie。
|
|
57
|
+
* 未登录时拒绝连接,客户端收到 connect_error(message 为 unauthorized)。
|
|
58
|
+
*/
|
|
59
|
+
export declare function authenticate(options?: {
|
|
60
|
+
guard?: string;
|
|
61
|
+
optional?: boolean;
|
|
62
|
+
}): SocketMiddleware;
|
|
63
|
+
/**
|
|
64
|
+
* 服务端推送(HTTP 接口、任务、事件监听器中):
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* SocketIO.to(`user:${id}`).emit('notice', data)
|
|
68
|
+
* SocketIO.of('/chat').emit('chat:message', text)
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export interface SocketIOFacade {
|
|
72
|
+
readonly server: Server;
|
|
73
|
+
emit: (event: string, ...args: unknown[]) => boolean;
|
|
74
|
+
to: (room: string | string[]) => ReturnType<Server['to']>;
|
|
75
|
+
except: (room: string | string[]) => ReturnType<Server['except']>;
|
|
76
|
+
of: (namespace: string) => Namespace;
|
|
77
|
+
}
|
|
78
|
+
export declare const SocketIO: SocketIOFacade;
|
|
79
|
+
/** 测试中释放(通常随 app.dispose() 自动释放) */
|
|
80
|
+
export declare function closeSocketIO(): Promise<void>;
|
|
81
|
+
export type { Namespace, Server, Socket };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createToken, definePersistent, registerServerExtension, unregisterServerExtension } from "@hazejs/core";
|
|
2
|
+
// src/index.ts
|
|
3
|
+
const kNamespace = Symbol.for("hazejs.socket.namespace");
|
|
4
|
+
/** `config/socketio.ts` 的默认导出 */
|
|
5
|
+
function defineSocketIOConfig(config) {
|
|
6
|
+
return config;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 定义命名空间(app/socket/*.ts 的默认导出,自动注册):
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* export default defineNamespace({
|
|
13
|
+
* middleware: [authenticate()],
|
|
14
|
+
* connection(socket, io) {
|
|
15
|
+
* socket.on('chat:send', (text, ack) => { io.of('/chat').emit('chat:message', text); ack?.('ok') })
|
|
16
|
+
* },
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
function defineNamespace(definition) {
|
|
21
|
+
return {
|
|
22
|
+
[kNamespace]: true,
|
|
23
|
+
middleware: [],
|
|
24
|
+
...definition
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function isNamespace(value) {
|
|
28
|
+
return typeof value === "object" && value !== null && kNamespace in value;
|
|
29
|
+
}
|
|
30
|
+
/** 注入令牌:`inject(SocketIOServer)` 取得 socket.io 的 Server */
|
|
31
|
+
const SocketIOServer = createToken("socket.io");
|
|
32
|
+
function state() {
|
|
33
|
+
return definePersistent("socket.io", () => ({
|
|
34
|
+
io: void 0,
|
|
35
|
+
starting: void 0,
|
|
36
|
+
app: void 0,
|
|
37
|
+
definitions: /* @__PURE__ */ new Map(),
|
|
38
|
+
wired: /* @__PURE__ */ new Set(),
|
|
39
|
+
close: []
|
|
40
|
+
}), { dispose: async (value) => {
|
|
41
|
+
await closeServer(value.io);
|
|
42
|
+
for (const close of value.close.splice(0)) await close();
|
|
43
|
+
value.io = void 0;
|
|
44
|
+
value.starting = void 0;
|
|
45
|
+
value.wired.clear();
|
|
46
|
+
} });
|
|
47
|
+
}
|
|
48
|
+
/** 关闭 socket.io:未挂到任何 HTTP 服务器(如测试中)时 io.close() 会抛错,只断开连接 */
|
|
49
|
+
async function closeServer(io) {
|
|
50
|
+
if (!io) return;
|
|
51
|
+
if (!io.engine) {
|
|
52
|
+
io.disconnectSockets(true);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await new Promise((resolve) => {
|
|
56
|
+
io.close(() => resolve()).catch(() => resolve());
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function namespaceName(file, definition) {
|
|
60
|
+
if (definition.name) return definition.name.startsWith("/") ? definition.name : `/${definition.name}`;
|
|
61
|
+
if (!file || file === "index") return "/";
|
|
62
|
+
return `/${file.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}`;
|
|
63
|
+
}
|
|
64
|
+
/** 按顺序执行中间件链 */
|
|
65
|
+
function runMiddleware(middleware, socket, done) {
|
|
66
|
+
let index = 0;
|
|
67
|
+
const next = (error) => {
|
|
68
|
+
if (error || index >= middleware.length) return done(error);
|
|
69
|
+
const current = middleware[index++];
|
|
70
|
+
Promise.resolve(current(socket, next)).catch(next);
|
|
71
|
+
};
|
|
72
|
+
next();
|
|
73
|
+
}
|
|
74
|
+
function isWorkers() {
|
|
75
|
+
return typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers";
|
|
76
|
+
}
|
|
77
|
+
async function createServer(app, current) {
|
|
78
|
+
const config = app.config.get("socketio", {});
|
|
79
|
+
const { Server } = await import("socket.io");
|
|
80
|
+
const path = config.path ?? "/socket.io/";
|
|
81
|
+
const cors = config.cors === false ? void 0 : config.cors ?? {
|
|
82
|
+
origin: app.config.get("cors.origin", void 0) ?? true,
|
|
83
|
+
credentials: true
|
|
84
|
+
};
|
|
85
|
+
const io = new Server({
|
|
86
|
+
path,
|
|
87
|
+
serveClient: false,
|
|
88
|
+
destroyUpgrade: false,
|
|
89
|
+
...cors && { cors },
|
|
90
|
+
...config.options
|
|
91
|
+
});
|
|
92
|
+
if (config.adapter?.driver === "redis") {
|
|
93
|
+
const [{ createAdapter }, ioredis] = await Promise.all([import("@socket.io/redis-adapter"), import("ioredis")]);
|
|
94
|
+
const pub = new (ioredis.Redis ?? ioredis.default)(config.adapter.url ?? "redis://127.0.0.1:6379");
|
|
95
|
+
const sub = pub.duplicate();
|
|
96
|
+
io.adapter(createAdapter(pub, sub, { key: config.adapter.key ?? "socket.io" }));
|
|
97
|
+
current.close.push(() => pub.quit(), () => sub.quit());
|
|
98
|
+
}
|
|
99
|
+
const prefix = path.replace(/\/$/, "");
|
|
100
|
+
const extension = {
|
|
101
|
+
name: "socket.io",
|
|
102
|
+
matches: (pathname) => pathname === prefix || pathname.startsWith(`${prefix}/`),
|
|
103
|
+
node: (server) => void io.attach(server)
|
|
104
|
+
};
|
|
105
|
+
if (globalThis.Bun) {
|
|
106
|
+
const { Server: Engine } = await import("@socket.io/bun-engine");
|
|
107
|
+
const engine = new Engine({
|
|
108
|
+
path,
|
|
109
|
+
...cors && { cors }
|
|
110
|
+
});
|
|
111
|
+
io.bind(engine);
|
|
112
|
+
const handler = engine.handler();
|
|
113
|
+
const registered = {
|
|
114
|
+
...extension,
|
|
115
|
+
bun: {
|
|
116
|
+
handle: (request, server) => engine.handleRequest(request, server),
|
|
117
|
+
owns: (ws) => typeof ws.data === "object" && ws.data !== null && "transport" in ws.data,
|
|
118
|
+
websocket: handler.websocket,
|
|
119
|
+
idleTimeout: handler.idleTimeout
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
registerServerExtension(registered);
|
|
123
|
+
current.close.push(() => unregisterServerExtension("socket.io", registered));
|
|
124
|
+
} else {
|
|
125
|
+
registerServerExtension(extension);
|
|
126
|
+
current.close.push(() => unregisterServerExtension("socket.io", extension));
|
|
127
|
+
}
|
|
128
|
+
return io;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Socket.IO 服务提供者(app/provider/SocketProvider.ts:`export default socketIoProvider()`):
|
|
132
|
+
* 创建 socket.io Server(跨热更新保留),注册 app/socket 下的命名空间,挂到框架的 HTTP 服务器。
|
|
133
|
+
* 新连接总是使用最新的命名空间定义;Edge / Serverless 运行时上不启动。
|
|
134
|
+
*/
|
|
135
|
+
function socketIoProvider() {
|
|
136
|
+
return { async register(container, app) {
|
|
137
|
+
if (isWorkers()) {
|
|
138
|
+
app.logger.warn("socket.io is not supported on Cloudflare Workers (use r.ws() with Durable Objects); socketIoProvider() is skipped");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const current = state();
|
|
142
|
+
current.app = app;
|
|
143
|
+
current.starting ??= createServer(app, current).then((io) => current.io = io);
|
|
144
|
+
const io = await current.starting;
|
|
145
|
+
container.instance(SocketIOServer, io);
|
|
146
|
+
for (const [file, definition] of app.discovered("sockets")) {
|
|
147
|
+
if (!isNamespace(definition)) throw new TypeError("app/socket/*.ts must export default defineNamespace(...)");
|
|
148
|
+
const name = namespaceName(file, definition);
|
|
149
|
+
current.definitions.set(name, definition);
|
|
150
|
+
if (current.wired.has(name)) continue;
|
|
151
|
+
current.wired.add(name);
|
|
152
|
+
const namespace = io.of(name);
|
|
153
|
+
namespace.use((socket, next) => runMiddleware(current.definitions.get(name)?.middleware ?? [], socket, next));
|
|
154
|
+
namespace.on("connection", (socket) => {
|
|
155
|
+
Promise.resolve(current.definitions.get(name)?.connection(socket, io, namespace)).catch((error) => {
|
|
156
|
+
current.app?.logger.error(`socket.io ${name} connection handler failed`, error);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
} };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 握手鉴权:按 config/auth.ts 的 Guard 识别用户,存入 `socket.data.user`。
|
|
164
|
+
* 令牌取 `io({ auth: { token } })` 或 Authorization 头;Session Guard 读握手请求的 Cookie。
|
|
165
|
+
* 未登录时拒绝连接,客户端收到 connect_error(message 为 unauthorized)。
|
|
166
|
+
*/
|
|
167
|
+
function authenticate(options = {}) {
|
|
168
|
+
return async (socket, next) => {
|
|
169
|
+
const app = state().app;
|
|
170
|
+
if (!app) return next(/* @__PURE__ */ new Error("socket.io is not configured"));
|
|
171
|
+
const { headers, url, auth } = socket.handshake;
|
|
172
|
+
const requestHeaders = new Headers();
|
|
173
|
+
for (const [key, value] of Object.entries(headers)) if (value !== void 0) requestHeaders.set(key, Array.isArray(value) ? value.join(", ") : String(value));
|
|
174
|
+
if (typeof auth?.token === "string") requestHeaders.set("authorization", `Bearer ${auth.token}`);
|
|
175
|
+
const target = /^https?:\/\//.test(url) ? url : `http://${headers.host ?? "localhost"}${url}`;
|
|
176
|
+
const user = await app.contextFor(new Request(target, { headers: requestHeaders })).auth.guard(options.guard).user().catch(() => void 0);
|
|
177
|
+
if (user === void 0 && !options.optional) return next(Object.assign(/* @__PURE__ */ new Error("unauthorized"), { data: { code: 401 } }));
|
|
178
|
+
socket.data.user = user;
|
|
179
|
+
next();
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function server() {
|
|
183
|
+
const io = state().io;
|
|
184
|
+
if (!io) throw new Error("socket.io is not running: add socketIoProvider() in app/provider");
|
|
185
|
+
return io;
|
|
186
|
+
}
|
|
187
|
+
const SocketIO = {
|
|
188
|
+
get server() {
|
|
189
|
+
return server();
|
|
190
|
+
},
|
|
191
|
+
emit: (event, ...args) => server().emit(event, ...args),
|
|
192
|
+
to: (room) => server().to(room),
|
|
193
|
+
except: (room) => server().except(room),
|
|
194
|
+
of: (namespace) => server().of(namespace)
|
|
195
|
+
};
|
|
196
|
+
/** 测试中释放(通常随 app.dispose() 自动释放) */
|
|
197
|
+
async function closeSocketIO() {
|
|
198
|
+
const current = state();
|
|
199
|
+
await closeServer(current.io);
|
|
200
|
+
current.io = void 0;
|
|
201
|
+
current.starting = void 0;
|
|
202
|
+
current.wired.clear();
|
|
203
|
+
}
|
|
204
|
+
export { SocketIO, SocketIOServer, authenticate, closeSocketIO, defineNamespace, defineSocketIOConfig, isNamespace, socketIoProvider };
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazejs/socket.io",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"description": "Socket.IO server integration for hazejs (namespaces, guard auth, Redis adapter, Node and Bun)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/penna-org/haze/tree/main/packages/socketio#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/penna-org/haze.git",
|
|
11
|
+
"directory": "packages/socketio"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/penna-org/haze/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"hazejs",
|
|
18
|
+
"haze",
|
|
19
|
+
"nodejs",
|
|
20
|
+
"typescript",
|
|
21
|
+
"socket.io",
|
|
22
|
+
"websocket",
|
|
23
|
+
"realtime"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.12.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@socket.io/bun-engine": "^0.1.2",
|
|
41
|
+
"@socket.io/redis-adapter": "^8.3.0",
|
|
42
|
+
"ioredis": "^6.0.0",
|
|
43
|
+
"socket.io": "^4.8.0",
|
|
44
|
+
"@hazejs/core": "^0.1.1"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@socket.io/bun-engine": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@socket.io/redis-adapter": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"ioredis": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@socket.io/bun-engine": "^0.1.2",
|
|
59
|
+
"@socket.io/redis-adapter": "^8.3.0",
|
|
60
|
+
"ioredis": "^6.0.0",
|
|
61
|
+
"socket.io": "^4.8.4",
|
|
62
|
+
"socket.io-client": "^4.8.4",
|
|
63
|
+
"@hazejs/core": "0.1.1",
|
|
64
|
+
"@hazejs/vite": "0.1.1"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsdown"
|
|
68
|
+
}
|
|
6
69
|
}
|